前端性能优化篇: 防抖和节流

31,688 阅读4分钟

防抖和节流

2021/5/5

参考文章

一. 为什么要用到防抖节流

  • 当函数绑定一些持续触发的事件如:resize、scroll、mousemove ,键盘输入,多次快速click等等,

  • 如果每次触发都要执行一次函数,会带来性能下降,资源请求太频繁等问题

  • 就比如这样

    • div{
      	height:150px;
      	line-height:150px;
      	text-align:center;
      	color: #fff;
      	background-color:#ccc;
      	font-size:80px;
      }
      
    • <div id="content"></div>
        <script>
          let num = 1;
          const content = document.getElementById('content');
          function count() {
            content.innerHTML = num++;
          };
          content.onmousemove = count;
        </script>
      
      
  • 上面的效果,只要鼠标在div区域内一移动,count函数就会被执行,数字就会增加。

  • 这时就可以用上防抖和节流了

二. 防抖

1. 什么是防抖?

  • 所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

2. 应用防抖

非立即执行版本
/**
 * 非立即执行防抖函数
 * @param {Function} func 
 * @param {number} delay 
 * @returns 
 */
function debounce(func, delay) {
   let timeout
   return function () {
       const _this = this
       const args = [...arguments]
       if (timeout) {
           clearTimeout(timeout)
       }
       timeout = setTimeout(() => {
           func.apply(_this, args)
       }, delay)
   }
}

使用示例一

/**
 * 模拟打印index
 * @param {number} index 
 */
function log(index) {
    console.log("index:", index)
}
// 延迟一秒打印
const logFun = debounce(log, 1000)
// 索引
let index1 = 1
// 模拟100毫秒触发一次
const interval1 = setInterval(() => {
    logFun(index1++)
}, 100)
// 模拟1秒后停止触发
setTimeout(() => {
    clearInterval(interval1)
}, 1000)

使用实例二

/**
 * 模拟修改car对象的frameNum属性
 * @param {string} frameNum 车架号
 */
function changeframeNum(frameNum) {
    this.frameNum = frameNum
    console.log(car)
}
const car = {
    name: 'byd',
    frameNum: '22112',
    changeframeNum: debounce(changeframeNum, 2000)
}
let index2 = 0
const interval2 = setInterval(() => {
    car.changeframeNum('2023' + index2++)
}, 100)
setTimeout(() => {
    clearInterval(interval2)
}, 1000)
  • 暂存 this 和 参数。

  • 功能函数通过调用apply方法将debounce 函数最终返回的函数 this 指向绑定给自身。

  • 不管是setTimeout还是setInterval,都会有一个返回值。这个返回值是一个数字,代表当前是在浏览器中设置的第几个定时器(返回的是定时器序号)。如果在编辑器返回值可能是个对象。

  • 每次触发调用防抖函数,如果之前的定时器(以定时器序号作为标识符)还在。就清除前面一个定时器,并开启一个新的定时器。

  • 定时器即使清除了,其返回值也不会清除,之后设置定时器的返回值也会在其返回值的基础上继续向后排。

立即执行版
  • 触发事件后函数会立即执行,n 秒内触发事件不会执行功能函数下一次调用,n秒后再次触发才会再次执行功能函数。
// 防抖函数,立即执行版本 
function debounce(func, wait) { 
    let timeout; 
    return function () { 
        const context = this; 
        const args = [...arguments]; 
        if (timeout) clearTimeout(timeout); 
        const callNow = !timeout; 
        timeout = setTimeout(() => { 
            timeout = null; 
        }, wait); 
        if (callNow) func.apply(context, args); 
    }; 
}
  • 只有在callnow值为true时才会触发功能函数

  • 第一次调用防抖函数,还没有定时器,触发功能函数

  • 在时间间隔内如果再次调用防抖函数,这时已经有定时器在了,即使清除定时器,但定时器标识符timeout还在,所以callNow的值是false,不能触发功能函数

  • 等定时器间隔过去后执行定时器里的代码,将定时器标识符timeout设置为null

  • 之后再调用防抖函数才会让callNow的值为true,触发功能函数

合并
  • 增加一个形参作为判断
  // 防抖函数,合并版本,immediate为true时为立即执行
  function debounce(func, wait, immediate) {
    let timeout;
    return function () {
      const context = this;
      const args = [...arguments];
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        const callNow = !timeout;
        timeout = setTimeout(() => {
          timeout = null;
        }, wait)
        if (callNow) func.apply(context, args)
      }
      else {
        timeout = setTimeout(() => {
          func.apply(context, args)
        }, wait);
      }
    }
  }
  

三. 节流

1. 什么是节流?

  • **所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。**节流会稀释函数的执行频率。

2. 应用节流

时间戳
  • function throttle(func, wait) {
        var previous = 0;
        return function() {
            let now = Date.now();
            let context = this;
            let args = arguments;
            if (now - previous > wait) {
                func.apply(context, args);
                previous = now;
            }
        }
    }
    content.onmousemove = throttle(count,1000);
    
  • 时间戳版的功能函数触发是在时间段内开始的时候

定时器
 function throttle(func, wait) {
     let timeout;
     return function() {
         let context = this;
         let args = arguments;
         if (!timeout) {
             timeout = setTimeout(() => {
                 timeout = null;
                 func.apply(context, args)
             }, wait)
         }
 
     }
 }

使用示例

function log(index) {
    console.log('index: ', index)
}
const logT = throttle(log, 1000)
let num = 0
// 模拟100毫秒触发一次
setInterval(() => {
    logT(num++)
}, 100)
  • 第一次调用节流函数,还没有定时器,创建一个定时器,并在时间间隔结束时触发功能函数

  • 在时间间隔内再次调用节流函数,由于定时器已经存在,不响应

  • 当时间间隔结束后将本定时器标识符timeout清除,再创建一个定时器。

  • 由于定时器标识符timeout被设置为null,再次调用节流函数便可再次触发。