探讨如何通过优化JavaScript代码来提高性能,包括减少重绘和重排、使用节流和防抖技术、使用性能分析工具等;
1 写好JS的一些原则:
- 各司其职:让HTML、CSS和JavaScript职能分离;应当避免不必要的由JS直接操作样式;用class来表示状态;纯展示类交互需求零JS方案
- 组件封装:好的UI组件具备正确性、扩展性、复用性
- 过程抽象:应用函数式编程思想
1.1 各司其职
1.1.1 介绍
1.1.2 案例分析
eg.深夜食堂
- 版本一:
// night_canteen.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>深夜食堂</title>
<link rel="stylesheet" href="../css/night_canteen.css" />
</head>
<body>
<header>
<button id="modeBtn">🌞</button>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="../img/async-sync.png" alt="" />
</div>
<div class="description">
<h2>什么是Async/Await?</h2>
<p>Async/await基于Promise构建,并且与所有现有的基于Promise的API兼容。</p>
<p>异步-声明一个异步函数(async function someName(){...})- 自动将常规功能转换为Promise;当调用异步函数时,将使用其体内返回的值进行解析;异步功能可启用await。</p>
<p>等待-暂停执行异步功能(var result = await someAsyncCall();)- 当放置在Promise调用之前,await强制其余代码等待,直到Promise完成并返回结果;Await仅适用于Promise,不适用于回调;等待只能在async函数内部使用。</p>
</div>
</main>
<script type="text/javascript" src="../JS/night_canteen.js"></script>
</body>
</html>
// night_canteen.css
body,
html {
width: 800px;
height: 1000px;
padding: 0;
margin: 0;
overflow: hidden;
border: 1px gray solid;
}
body {
padding: 20px;
box-sizing: border-box;
}
div.pic img {
width: 100%;
}
#modeBtn {
font-size: 20px;
float: right;
border: none;
background: transparent;
}
// night_canteen.js
const btn = document.getElementById('modeBtn')
btn.addEventListener('click', (e) => {
const body = document.body
if (e.target.innerHTML === '🌞') {
body.style.backgroundColor = 'black'
body.style.color = 'white'
e.target.innerHTML = '🌜'
} else {
body.style.backgroundColor = 'white'
body.style.color = 'black'
e.target.innerHTML = '🌞'
}
})
未点击按钮:
点击按钮:
分析:以上代码实现该案例的全部功能,这里使用js修改页面的样式及表现,但是一般来说,都是通过css来实现样式的改变,这违背了各司其职的原则。 于是,对以上代码进行优化处理。
- 版本二:
// 需要修改代码,如下
<!-- <button id="modeBtn">🌞</button> -->
<button id="modeBtn"></button>
// css文件中需要添加代码,如下:
body.night {
background-color: black;
color: white;
transition: all 1s;
}
#modeBtn::after {
content: '🌞';
}
body.night #modeBtn::after {
content: '🌜';
}
const btn = document.getElementById('modeBtn')
btn.addEventListener('click', (e) => {
const body = document.body
if (body.className !== 'night') {
// 判断是否有night类
body.className = 'night' // 没有night类名,则是白天,添加night类
} else {
body.className = '' // 当前是night,则去除night类
}
})
分析:在该版本代码中,js直接操控只有body的状态,将添加night属性的任务交给了css。也就是说,当不存在night属性时,点击按钮,添加night属性,如果已存在night属性,则删除night属性。对样式的操作修改全部由css完成,符合JavaScript编写的各司其职原则。
- 版本三:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>深夜食堂</title>
<link rel="stylesheet" href="../css/night_canteen.css" />
</head>
<body>
<input id="modeCheckBox" type="checkbox" />
<div class="content">
<header>
<!-- <button id="modeBtn">🌞</button> -->
<label id="modeBtn" for="modeCheckBox"></label>
<h1>深夜食堂</h1>
</header>
<main>
<div class="pic">
<img src="../img/async-sync.png" alt="" />
</div>
<div class="description">
<h2>什么是Async/Await?</h2>
<p>Async/await基于Promise构建,并且与所有现有的基于Promise的API兼容。</p>
<p>异步-声明一个异步函数(async function someName(){...})- 自动将常规功能转换为Promise;当调用异步函数时,将使用其体内返回的值进行解析;异步功能可启用await。</p>
<p>等待-暂停执行异步功能(var result = await someAsyncCall();)- 当放置在Promise调用之前,await强制其余代码等待,直到Promise完成并返回结果;Await仅适用于Promise,不适用于回调;等待只能在async函数内部使用。</p>
</div>
</main>
</div>
<script type="text/javascript" src="../JS/night_canteen.js"></script>
</body>
</html>
body,
html {
width: 800px;
/* height: 1000px; */
padding: 0;
margin: 0;
overflow: hidden;
border: 1px gray solid;
}
body {
/* padding: 20px; */
box-sizing: border-box;
}
div.pic img {
width: 100%;
}
.content {
padding: 10px;
transition: background-color 1s, color 1s;
}
#modeCheckBox {
display: none;
}
#modeCheckBox:checked + .content {
background-color: black;
color: white;
transition: all 1s;
}
#modeBtn {
font-size: 2rem;
float: right;
}
#modeBtn::after {
content: '🌞';
}
#modeCheckBox:checked + .content #modeBtn::after {
content: '🌜';
}
// 这个需求是纯展示类交互,没有js代码且完整实现了上面的需求了,这里重新修改了HTML文件的结构,使用<label>、<input type='checkbox'>标签的特性,在css里面使用伪类:checked及兄弟选择器完成了零代码实现需求的方案
分析:
1.2 组件封装
1.2.1 介绍
- 组件是指Web页面上抽出来一个个包含模板(HTML)、功能(JS)和样式(CSS)的单元。好的组件具备封装性、正确性、扩展性和复用性
- 组件封装总结: 组件设计原则:封装性、正确性、扩展性、复用性 实现组件的步骤:结构设计、展现效果、行为设计 三次重构:插件化、模板化、抽象化(组件框架)
1.2.2 案例分析
eg.用原生JS写一个电商网站的轮播图
- 版本一(API无交互版)
// 轮播图是一个典型的列表结构,可以使用无序列表<ul>元素来实现
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>轮播图——Carousel figure</title>
</head>
<body>
<div class="slider-list" id="my-slider">
<ul>
<li class="slider-list__item--selected"><img src="../img/JDimg01.jpg" alt="" /></li>
<li class="slider-list__item"><img src="../img/JDimg02.jpg" alt="" /></li>
<li class="slider-list__item"><img src="../img/JDimg03.jpg" alt="" /></li>
<li class="slider-list__item"><img src="../img/JDimg04.jpg" alt="" /></li>
<li class="slider-list__item"><img src="../img/JDimg07.webp" alt="" /></li>
</ul>
<a href="" class="slider-list__next"></a>
<a href="" class="slider-list__previous"></a>
<div class="slider-list__control">
<span class="slider-list__control-buttons--selected"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
<span class="slider-list__control-buttons"></span>
</div>
</div>
</body>
</html>
// 使用CSS绝对定位将图片重叠在同一个位置;轮播图切换的状态使用修饰符--selected;轮播图的切换动画使用CSS transition
class Slider {
// constructor({ container }) {
// this.container = container
// this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected')
// }
constructor(id, cycle = 3000) {
this.container = document.getElementById(id)
this.items = this.container.querySelectorAll('.slider-list__item, .slider-list__item--selected')
this.cycle = cycle
const controller = this.container.querySelector('.slider-list__control')
const buttons = controller.querySelectorAll('.slider-list__control-buttons, .slider-list__control-buttons--selected')
//当鼠标移入小圆点事件
controller.addEventListener('mouseover', (evt) => {
const idx = Array.from(buttons).indexOf(evt.target)
if (idx >= 0) {
this.slideTo(idx)
this.stop() // 停止自动循环播放
}
})
//当鼠标移出小圆点事件
controller.addEventListener('mouseout', (evt) => {
this.start() // 开始自动循环播放
})
//注册slide事件,将选中的图片和小圆点设置为selected状态
this.container.addEventListener('slide', (evt) => {
const idx = evt.detail.index
const selected = controller.querySelector('.slider-list__control-buttons--selected')
if (selected) selected.className = 'slider-list__control-buttons'
buttons[idx].className = 'slider-list__control-buttons--selected'
})
//当用户点击上一张时,停止定时器,然后执行slidePrevious()方法,让图片向前翻一张,然后重启定时器
const previous = this.container.querySelector('.slider-list__previous')
previous.addEventListener('click', (evt) => {
this.stop()
this.slidePrevious()
this.start()
evt.preventDefault()
})
//当用户点击下一张时,先停止定时器,然后向后翻一张,再重启定时器
const next = this.container.querySelector('.slider-list__next')
next.addEventListener('click', (evt) => {
this.stop()
this.slideNext()
this.start()
evt.preventDefault()
})
}
// 获取被选中的元素
getSelectedItem() {
const selected = this.container.querySelector('.slider-list__item--selected')
return selected
}
// 返回选中的元素在items数组中的位置
getSelectedItemIndex() {
return Array.from(this.items).indexOf(this.getSelectedItem())
}
slideTo(idx) {
const selected = this.getSelectedItem()
if (selected) {
selected.className = 'slider-list__item'
}
const item = this.items[idx]
if (item) {
item.className = 'slider-list__item--selected'
}
const detail = { index: idx }
const event = new CustomEvent('slide', { bubbles: true, detail })
this.container.dispatchEvent(event)
}
// 将下一张图片标记为选中状态
slideNext() {
const currentIdx = this.getSelectedItemIndex()
const nextIdx = (currentIdx + 1) % this.items.length
this.slideTo(nextIdx)
}
// 将上一张图片标记为选中状态
slidePrevious() {
const currentIdx = this.getSelectedItemIndex()
const previousIdx = (this.items.length + currentIdx - 1) % this.items.length
this.slideTo(previousIdx)
}
// 4 实现用户控制
start() {
this.stop()
this._timer = setInterval(() => this.slideNext(), this.cycle)
}
stop() {
clearInterval(this._timer)
}
start() {
this.stop()
this._timer = setInterval(() => this.slideNext(), this.cycle)
}
stop() {
clearInterval(this._timer)
}
}
const container = document.querySelector('.slider-list')
const slider = new Slider('my-slider')
slider.start()
setInterval(() => {
slider.slideNext()
}, 3000)
// 作者:皓月u
// 链接:https://juejin.cn/post/7125704569408978981
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
关于CSS命名:采用了BEM(Block Element Modifier)规范,这一规范采用了以下三个部分来描述规则,让代码更利于维护。分别是:Block(逻辑和功能独立的单元,类似于组件);Element(block的组成部分);Modifier(修饰符,用于修饰块或元素,体现出外观、行为、状态等特征)
首先是Block,因为这个组件是实现轮播图的功能逻辑,所以这个组件class属性为slider-list。然后是Element,比如对应的列表项li元素,表示item,Block和Element之间使用双下划线__连接,所以它的class属性是slider-list__item。最后,Modifier表示状态,其中一个列表的状态是selected,Element和Modifier之间使用双横杠--连接,所以最终的class是slider-list__item--selected。
参考文章:juejin.cn/post/712570…
关于行为设计(JS):[API设计应保证原子操作、职责单一,满足灵活性]。
根据组件要实现的需求,设计了几个API:[getSelectedItem()--获取选中的图片、getSelectedItemIndex()--获取选中的图片位置、slideTo()--切换到某张图片、slideNext()--切换到下一张图、slideNext()--切换到上一张图、slidePrevious()]
1.3 过程抽象
1.3.1 介绍
- 用来处理局部细节控制的一些方法;函数式编程思想的基础应用
- 高阶函数:
Once:为了能够让“只执行一次”的需求覆盖不同的事件处理,可以将该需求剥离出来,这个过程称为过程抽象
HOF:以函数作为参数;以函数作为返回值;常用于作为函数装饰器function once(fn){ return function(...args){ if(fn){ const ret = fn.apply(this, args) fn = null return ret } } }function HOF0(fn){ return function(...args){ return fn.apply(this, args) } }