函数防抖与函数节流

1,963 阅读5分钟

防抖 ( debounce ) 节流 ( throttle ) 优化高频 JavaScript 代码的一种手段 ; 比如 鼠标的 mouseover mousemove 事件 , 浏览器的 scroll resize 事件 , 还有 输入框的 keypress keyup keydown 等事件在触发时 , 会不断的执行事件函数 , 浪费性能资源 ; 防抖 节流 就是对这类事件进行调用次数的限制 , 对性能做出一定的优化 ;

防抖 debounce

在事件被触发 n 秒后 , 再次执行回调 ; 如果 n 秒内触发 , 重新计时 ;

想象一下 , 一个搜索联想功能 ; 你使用了 keyup 事件去监听用户弹起键盘后, 你请求接口获取联想数据 ; 然后渲染视图 ; 此时就会出现一个问题 , 只要你输入了内容就会立即调用接口渲染视图 ; 如果用户连续输入多个关键词 , 而你频繁的调用多次会出现什么问题呢 ?

搜索联想这样搞肯定是不合适的 ;

问题分析 :

  • 事件回调执行太快了, 键盘弹起立马就触发了 ; 我们需要 n 秒后再去执行回调 ;
  • 如果我们输入正在连续内容的 n 秒内, 最好不要去执行回调 ;

思路分析 :

  • 可以键盘弹起一定时间后再去执行回调 ;
  • 给用户一个 300ms 的延时 , 如果 300ms 不输入内容, 就去调接口渲染视图 ;
  • 如果第一次输入内容后, 时间快要到达 300ms 用户再次输入 , 我们重新计算时间 ;

将目标方法 ( 动作 ) 包装再 setTimeout 中 , 然后这个方法是一个事件的回调函数 , 如果这个回调一直执行 , 那么这些动作就一直不执行 ; 利用 clearTimeout 将事件内的连续动作删掉 , 用户不在触发这个事件的时候 , 再将 setTimeout 执行 ;

总结 在事件被触发 n 秒后 , 再次执行回调 ; 如果 n 秒内触发 , 重新计时 ;

// func 要执行的防抖函数
function debounce(func, delay) {
    delay = delay || 300;
 var timer = null;
    // return 一个 function 利用闭包维护 timer 和 delay; 
    // 另外这个也是一个处理过防抖的函数;
    return function() {
        var _this = this;
        var arg = arguments;
        clearTimeout(timer); // 每次用户输入清空上一个 timer 这样能保证下面的定时器每次都重新开始
        timer = setTimeout(function() {
            // 解决事件对象找不到和this指向错误的问题
            func.apply(_this, arg);
        }, delay);
    }
}
// 不要在意风格不统一, 演示而已 😂 
let input = document.querySelector('input')
input.onkeyup = debounce(async function () {
  let { data } = await axios.post('/v1/api/index/hits', {
    words: input.value
  })
  render(data.data)
})
  • 为什么要在外部维护一个 this 和 arguments , 而不是在里面直接用

    • 可以看出 setTimeout 里面是一个匿名函数 , 通过 function 关键字声明的函数 , this 会指向 window , 这显然是不可以的 ; debounce 实际执行的是 return function , 所以外部维护这个 this 应该是没有毛病的 ;

    • arguments 是用来接收不固定参的 , 如果我们直接接收 arguments 其实接收的是 setTimeout 中的不固定参 , 这显然也是不合理的 ;

  // 如果不想在外部维护 this 和 arguments 也很简单
  function debounce(func, delay=300) {
    let timer = null;
    return function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => {
        func.call(this, ...args);
      }, delay);
    }
  }
  • 为什么使用 call 方法 ?

    • 允许为不同的对象分配和调用属于一个对象的函数/方法 . 提供新的 this 值给当前调用的函数/方法。 详细看 MDN

节流 throttle

高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率

嗯 , 这个相对还是比较好理解的 ; 就是一直执行的事件或者函数咱们给他限流一下 ; 像不像早高峰限流的地铁 ? 你就是限流的事件或者方法 🤣

假设, 我们现在需要做个 检测浏览器滚动条滚动的功能 ; 这个太简单, easy

window.addEventListener('scroll', function () { console.log('scroll...') });

这触发频率看着有些太快了; 大部分场景下我们完全没有必要监控频率这么高 , 既然谈节流, 那肯定是不想这样快的触发;下面就看实现

function throttle(func, delay=1000) {
    let timer = null;
    return function (...args) {
        if (timer) return; // 只要定时器执行就 return
        timer = setTimeout(() => {
            func.call(this, ...args);
            timer = null; // 将 timer 重置为 false
        }, delay);
    }
}
window.addEventListener('scroll', throttle(function(e) { console.log('scroll', this, e) }))

这里可以看出, 执行频率已经降下来了, 但是下面再看下, 有没有什么其他的问题呢 ?

可以看出先先滚动 , 再执行 ; 这样有问题吗 ? 其实也没啥问题 咱们再回顾一下概念 高频事件触发,但在n秒内只会执行一次,所以节流会稀释函数的执行频率 n 秒内执行一次 , 那是 n 秒的开始执行 , 还是 n 秒的结束执行呢 ? 当然我们希望这个是可控的 ;

function throttle(func, delay=1000, start=true) {
  let timer = null;
  return function(...args) {
    if (timer) return;
    start && func.call(this, ...args); // 短路运算, 是否立即执行 
    timer = setTimeout(() => {
    !start && func.call(this, ...args); // 短路运算, 是否定时器结束后执行
      timer = null;
    }, delay);
  }
}
  • start 为 true
  • start 为 false
  • 为什么立即执行, 放在这个位置 ;

    • 第一次执行时 , timer 条件不成立 , start 为 true 直接执行后面的函数 ;
    • 第二次执行时 ( 发生了滚动 ) , 执行第一个短路运算 , 进入定时器 , 由于定时器为异步 所以此时 timer 为 true , 就一直 return 异步销毁 timer 执行完毕 ;
  • 既然要销毁 timer 为什么不用 clearTimeout

    • 这里 timer 实际上就是一个标识而已 ; 标记 timer 是否为 true 是否可以执行 定时器
  function throttle(func, delay=1000) {
    let timer = 0;
    return function() {
      if (timer) return; // 不满足条件, timer + 1后满足, 就一直 return
      timer++;
      setTimeout(() => {
          func();
          timer = 0; // 异步执行完毕后, 重复上面的操作
      });
    }
  }

总结

防抖 ( debounce ) 节流 ( throttle ) 目的都是降低 回调的执行频率 , 达到节省资源的目的 ;

应用场景

防抖 ( debounce ) : 搜索联想 , 手机号检测等 ;

节流 ( throttle ) : 滚动加载 监听 , 窗口大小监听等 ;

参考文章

https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/5

https://segmentfault.com/a/1190000018445196

https://zhuanlan.zhihu.com/p/38313717

https://www.jianshu.com/p/4e840b7ed35b

https://www.jianshu.com/p/53feee9f4fad

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/call

本文使用 mdnice 排版