防抖与节流:实践与勘误

防抖与节流:实践与勘误

原文链接

前言

一般对于监听某些密集型键盘、鼠标、手势事件需要和后端请求交互、修改 dom 的,防抖、节流就很有必要了。

防抖

使用场景

  • 关键字远程搜索下拉框
  • resize

对于这类操作,一般希望拿到用户最终输入的关键字、确定的拖拽大小,然后与服务器交互。
而中间态的值,并不关心,为了减轻服务器压力,避免服务器资源浪费,这时就需要防抖了。

案例

  • 输入框防抖
// 记录时间
let last = Date.now();
  //模拟一段ajax请求
function ajax(content) {
    
    
  const d = Date.now();
  const span = d - last;
  console.log(`${
      
      content} 间隔 ${
      
      span}ms`);
  last = d;
}

const noActionInput = document.getElementById('noAction');

noActionInput.addEventListener('keyup', function(e) {
    
    
  ajax(e.target.value);
});
  • 未防抖
    在这里插入图片描述

可以看到为太多的中间态发送太多的请求。

/**
* 一般利用闭包存储和私有化定时器 `timer`
* 在 `delay` 时间内再次调用则清除未执行的定时器
* 重新定时器
* @param fn
* @param delay
* @returns {Function}
*/
function debounce(fn, delay) {
    
    
  let timer = null;
  return function() {
    
    
    // 中间态一律清除掉
    timer && clearTimeout(timer);
    // 只需要最终的状态,执行
    timer = setTimeout(() => fn.apply(this, arguments), delay);
  };
}
    
const debounceInput = document.getElementById('debounce');

let debounceAjax = debounce(ajax, 100);

debounceInput.addEventListener('keyup', function(e) {
    
    
  debounceAjax(e.target.value);
});
  • 防抖后

在这里插入图片描述

可以发现:

  1. 如果输入的很慢,差不多每 delay 100ms 执行一次;
  2. 如果输入的很快,说明用户在连续性输入,则会等到用户差不输入完慢下来了在执行回调。

节流

使用场景

  • 滑动滚动条
  • 射击类游戏发射子弹
  • 水龙头的水流速

对于某些连续性的事件,为了表现平滑过渡,这时的中间态我们也需要关心的。
但减弱密集型事件的频率依旧是性能优化的杀器。

勘误

非常常见的两种错误写法,太流行了,忍不住出来勘误。

// 时间戳版
function throttleError1(fn, delay) {
    
    
  let lastTime = 0;
  return function () {
    
    
    const now = Date.now();
    const space = now - lastTime; // 时间间隔,首次会是很大一个值
    if (space > delay) {
    
    
      lastTime = now;
      fn.apply(this, arguments);
    }
  };
}}

// 定时器版
function throttleError2(fn, delay) {
    
    
  let timer;
  return function () {
    
    
    if (!timer) {
    
    
      timer = setTimeout(() => {
    
    
        timer = null;
        fn.apply(this, arguments);
      }, delay);
    }
  };
}

这两个版本都有的问题,先假设 delay=100ms,假设定时器都是按时执行的。

  • 时间戳版
  1. 由于首次 now - lastTime === now 该值很大,首次 0ms 立即执行,用户接在 0-100ms 内执行的交互均无效,假如用户停留在 99ms,则最后一次丢失了。
  2. 例如要用滚动条离顶部的高度来设置样式,滚动条在 99ms 从 0 滚动到 100px 处,你没办法处理。
  • PS: 时间戳版,有一个应用场景,在一定时间内防止重复提交。

  • 定时器版

  1. 首次 0ms 不会立即执行有 100ms 延迟,好比开第一枪需要 100ms 后子弹才能出来。
  • 聪明的读者,可能想到了,可以结合两者来解决问题。
  1. 首次 0ms 立即执行无延迟;
  2. 获取最后状态,保证最后一次得到执行。

案例

  • 滚动滑动条时视觉上连续调整 dom
/**
* 时间戳来处理首次和间隔执行问题
* 定会器来确保最后一次状态改变得到执行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
    
    
  let timer, lastTime;
  return function() {
    
    
    const now = Date.now();
    const space = now - lastTime; // 间隔时间
    if( lastTime && space < delay ) {
    
     // 为了响应用户最后一次操作
      // 非首次,还未到时间,清除掉计时器,重新计时。
      timer && clearTimeout(timer);
      // 重新设置定时器
      timer = setTimeout(() => {
    
    
        lastTime = Date.now(); // 不要忘了记录时间
        fn.apply(this, arguments);
      }, delay);
      return;
    }
    // 首次或到时间了
    lastTime = now;
    fn.apply(this, arguments);
  };
}

const throttleAjax = throttle(ajax, 100);

window.addEventListener('scroll', function() {
    
    
  const top = document.body.scrollTop || document.documentElement.scrollTop;
  throttleAjax('scrollTop: ' + top);
});
  • 节流前

回调过于密集。(PS:经常听到 scroll 自带节流,就是指一帧 16ms 左右触发一次)

在这里插入图片描述

  • 节流后

可以发现,无论你滑的慢还是快都类似于定时触发。

在这里插入图片描述

  • 细心的读者可能会发现,假如交互停留在 199ms,定时器在 300ms 段才执行,间隔了约 200ms,定时器延迟不应该设置为原来的 delay

在这里插入图片描述

/**
* 时间戳来处理首次和间隔执行问题
* 定会器来确保最后一次状态改变得到执行
* @param fn
* @param delay
* @returns {Function}
*/
function throttle(fn, delay) {
  let timer, lastTime;
  return function() {
    const now = Date.now();
    const space = now - lastTime; // 间隔时间
    if( lastTime && space < delay ) { // 为了响应用户最后一次操作
      // 非首次,还未到时间,清除掉计时器,重新计时。
      timer && clearTimeout(timer);
      // 重新设置定时器
      timer = setTimeout(() => {
        lastTime = Date.now(); // 不要忘了记录时间
        fn.apply(this, arguments);
-      }, delay);
+      }, delay - space);
      return;
    }
    // 首次或到时间了
    lastTime = now;
    fn.apply(this, arguments);
+    // 当前已执行,清除掉计时器,不清除会有多余的中间执行
+    timer && clearTimeout(timer);
  };
}
  • 如果忘了最后清除

在这里插入图片描述

  • 最终效果

在这里插入图片描述

  • 实际生产还是使用 lodash 成熟可靠的的防抖节流实现。

  • lodash 效果

在这里插入图片描述

  • 查看 lodash 源码可以发现节流,是靠 leading 来控制首次是否需要执行,trailing 来控制 99ms 停止 100ms时需不需要执行,
    maxWait 来控制定时执行,看完本篇去分析,是不是就很好理解了呢。

  • 举几个常用的 lodash 使用方式。

  1. 实现类似 scroll 自带一帧 16ms 效果:throttle(fn, /* wait = undefined */),其实内部用到了 requestAnimationFrame
  2. 非常常用,发请求防止重复提交,例如首次点击执行,500ms 内的点击一律不执行: debounce(fn, 500, { leading: true, trailing: false })
  3. 例如某个 dom 被清除,debounced.cancel() 来取消最后一次(trailing)调用,避免取不到 dom 报错。
  4. 等等。

总结

防抖、节流都是利用闭包来实现内部数据获取与维护。
防抖比较好理解,节流就需要稍微需要思考下。两者还是有区别的,就不要一错再错,粘贴传播问题代码啦。
防抖、节流对于频繁dom事件性能优化是不可或缺的手段。

参考

  1. 文中 demo
  2. 7分钟理解JS的节流、防抖及使用场景

猜你喜欢

转载自blog.csdn.net/guduyibeizi/article/details/104753248