性能优化与调试技巧(如何写好JavaScript) | 青训营

63 阅读6分钟

探讨如何通过优化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 = '🌞'
  }
})

未点击按钮:
image.png
点击按钮:
image.png
分析:以上代码实现该案例的全部功能,这里使用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
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

image.png 关于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:为了能够让“只执行一次”的需求覆盖不同的事件处理,可以将该需求剥离出来,这个过程称为过程抽象
    function once(fn){
        return function(...args){
            if(fn){
                const ret = fn.apply(this, args)
                fn = null
                return ret
            }
        }
    }
    
    HOF:以函数作为参数;以函数作为返回值;常用于作为函数装饰器
    function HOF0(fn){
        return function(...args){
            return fn.apply(this, args)
        }
    }
    

image.png