js防抖和节流

213 阅读3分钟

防抖节流

防抖和节流是一种优化高频执行js的手段。比如我们需要监听鼠标的scroll事件,然后做某些事情,但是scroll事件触发非常频繁,如果每次都执行回调,势必会有性能损耗。又比如有一个输入框,我们需要监听用户的输入内容,内容变化去请求接口获取数据,但是如果用户以极快的速度输入内容,那么中间请求接口返回的数据就将被覆盖,完全没有必要,这也极大浪费了资源,降低了性能。诸如此类的情况非常多,所以为了优化体验,提高性能,需要对类事件的调用次数做限制,这就是防抖和节流。

两者的区别是防抖是在一定时间内连续触发的事件只执行最后一次,而函数节流则在一段时间内只执行一次。

举个现实中的🌰:

防抖就像我们坐公交车,司机会等所有乘客都上车之后,等一会儿确定没有人之后就会开车。而节流就像坐地铁,停留规定的时间,到点就开车。

防抖

应用场景

  • 输入框搜索
  • 按钮的重复点击
  • 上拉滚动加载
  • 用户的缩放事件
  • ......

实现原理

防抖.jpeg

  • 当事件触发时并不会立即执行回调,而是会等待一段时间
  • 如果在等待期间再次触发事件,会重新继续等待
  • 只有等待期间无新的事件触发才会执行回调

代码实现

function debounce(func, wait = 0, immediate = false, callback = () => {}) {
  let timer;
  //是否第一次调用过了
  let immediateInvoked = false;
  const debounced = function (...args) {
    return new Promise((resolve, reject) => {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      if (immediate && !immediateInvoked) {
        try {
          const result = func.apply(this, args);
          callback(null, result);
          resolve(result);
        } catch (e) {
          callback(e);
          reject(e);
        }
        immediateInvoked = true;
      }
      timer = setTimeout(() => {
        try {
          let result = func.apply(this, args);
          callback(null, result);
          resolve(result);
        } catch (e) {
          callback(e);
          reject(e);
        }
        immediateInvoked = false;
      }, wait);
    });
  };
  
  // 外部可以通过调用cancel方法取消
  debounced.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };
  return debounced;
}

节流

应用场景

  • 下拉刷新
  • 鼠标移动
  • 拖拽组件
  • ......

实现原理

节流.jpg

代码实现

/**
 *
 * @param {*} func
 * @param {*} wait
 * @param {*} options
 *   leading 是否要执行第一次,第一次触发的时候必须执行回调
 *   trailing 是否要执行最后一次
 * @returns
 */
function throttle(func, wait = 0, options = { leading: true, trailing: true, callback: () => {} }) {
  const { leading, trailing } = options;
  //上次回调的执行时间
  let lastExecTime = 0;
  let timer;
  const throttled = function (...args) {
    return new Promise((resolve, reject) => {
      //当前的时间戳
      const currentTime = Date.now();
      //如果说lastExecTime为0说明是第1次,并且第1次不要执行
      if (lastExecTime === 0 && !leading) {
        lastExecTime = currentTime;
      }
      //上次执行的时间加上等待的时间就是下一次要执行的时候
      const nextExecTime = lastExecTime + wait;
      if (currentTime >= nextExecTime) {
        if (timer) {
          clearTimeout(timer);
          timer = null;
        }
        try {
          let result = func.apply(this, args);
          callback(null, result);
          resolve(result);
        } catch (e) {
          callback(e);
          reject(e);
        }
        lastExecTime = currentTime;
      } else {
        if (trailing) {
          if (timer) {
            clearTimeout(timer);
            timer = null;
          }
          timer = setTimeout(() => {
            try {
              let result = func.apply(this, args);
              callback(null, result);
              resolve(result);
            } catch (e) {
              callback(e);
              reject(e);
            }
            lastExecTime = Date.now();
          }, nextExecTime - currentTime);
        }
      }
    });
  };
  // 外部可以通过调用cancel方法取消
  throttled.cancel = function () {
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
  };
  return throttled;
}