何为去抖函数? 在学习 JavaScript 去抖函数之前我们需要先弄明白这个概念很多人都会把去抖跟节流两个概念弄混, 但是这两个概念其实是很好理解的
去抖函数(Debounce Function), 是一个可以限制指定函数触发频率的函数我们可以理解为连续调用同一个函数多次, 只得到执行该函数一次的结果; 但是隔一段时间再次调用时, 又可以重新获得新的结果, 具体这段时间有多长取决于我们的设置这种函数的应用场景有哪些呢?
比如我们写一个 DOM 事件监听函数,
- window.onscroll = function(){
- console.log('Got it!');
- }
现在当我们滑动鼠标滚轮的时候, 我们就可以看到事件被触发了但是我们可以发现在我们滚动鼠标滚轮的时候, 我们的控制台在不断的打印消息, 因为 window 的 scroll 事件被我们不断的触发了
在当前场景下, 可能这是一个无伤大雅的行为, 但是可以预见到, 当我们的事件监听函数 (Event Handler) 涉及到一些复杂的操作时 (比如 Ajax 请求 DOM 渲染大量数据计算), 会对计算机性能产生多大影响; 在一些比较老旧的机型或者较低版本的浏览器(尤其 IE) 中, 很可能会导致死机情况的出现所以这个时候我们就要想办法, 在指定时间段内, 只执行一定次数的事件处理函数
理解去抖函数
说了一些概念和应用场景, 但是还是很拗口, 到底什么是去抖函数?
我们可以通过如下实例来理解:
假设有以下代码:
- var debounce = function(callback, delay, immediate){
- var timeout, result;
- return function(){
- var callNow;
- if(timeout)
- clearTimeout(timeout);
- callNow = !timeout && immediate;
- if(callNow) {
- result = callback.apply(this, Array.prototype.slice.call(arguments, 0));
- timeout = {};
- }
- else {
- timeout = setTimeout(()=>{
- callback.apply(this, Array.prototype.slice.call(arguments, 0));
- }, delay);
- }
- };
- };
- var s = debounce(()=>{
- console.log('yes...');
- }, 2000);
- window.onscroll = s;
debounce 函数就是我自己实现的一个简单的去抖函数, 我们可以通过这段代码进行实验
步骤如下:
复制以上代码, 打开浏览器, 打开控制台(F12), 然后粘贴代码并回车执行
连续不断的滚动鼠标, 查看控制台有无输出
停止滚动鼠标, 2s 之内再次滚动鼠标, 查看是否有输出
连续滚动之后停止 2s 以上, 查看是否有输出
通过以上步骤, 我们可以发现当我们连续滚动鼠标时, 控制台没有消息被打印出来, 停止 2s 以内并再次滚动时, 也没有消息输出; 但是当我们停止的时间超过 2s 时, 我们可以看到控制台有消息输出
这就是去抖函数在连续的触发中(无论时长), 只能得到触发一次的效果在指定时间长度内连续触发, 最多只能得到一次触发的效果
underscore 的实现
underscore 源码如下(附代码注释):
- // Returns a function, that, as long as it continues to be invoked, will not
- // be triggered. The function will be called after it stops being called for
- // N milliseconds. If `immediate` is passed, trigger the function on the
- // leading edge, instead of the trailing.
- // 去抖函数, 传入的函数在 wait 时间之后 (或之前) 执行, 并且只会被执行一次
- // 如果 immediate 传递为 true, 那么在函数被传递时就立即调用
- // 实现原理: 涉及到异步 JavaScript, 多次调用_.debounce 返回的函数, 会一次性执行完, 但是每次调用
- // 该函数又会清空上一次的 TimeoutID, 所以实际上只执行了最后一个 setTimeout 的内容
- _.debounce = function (func, wait, immediate) {
- var timeout, result;
- var later = function (context, args) {
- timeout = null;
- // 如果没有传递 args 参数, 那么 func 不执行
- if (args) result = func.apply(context, args);
- };
- // 被返回的函数, 该函数只会被调用一次
- var debounced = restArgs(function (args) {
- // 这行代码的作用是清除上一次的 TimeoutID,
- // 使得如果有多次调用该函数的场景时, 只执行最后一次调用的延时
- if (timeout) clearTimeout(timeout);
- if (immediate) {
- //// 如果传递了 immediate 并且 timeout 为空, 那么就立即调用 func, 否则不立即调用
- var callNow = !timeout;
- // 下面这行代码, later 函数内部的 func 函数注定不会被执行, 因为没有给 later 传递参数
- // 它的作用是确保返回了一个 timeout
- timeout = setTimeout(later, wait);
- if (callNow) result = func.apply(this, args);
- } else {
- // 如果没有传递 immediate, 那么就使用_.delay 函数延时执行 later
- timeout = _.delay(later, wait, this, args);
- }
- return result;
- });
- // 该函数用于取消当前去抖效果
- debounced.cancel = function () {
- clearTimeout(timeout);
- timeout = null;
- };
- return debounced;
- };
可以看到 underscore 使用了闭包的方法, 定义了两个私有属性: timeout 和 result, 以及两个私有方法 later 和 debounced 最终会返回 debounced 作为处理之后的函数 timeout 用于接受并存储 setTimeout 返回的 TimeoutID,result 用于执行用户传入的 func 函数的执行结果, later 方法用于执行传入的 func 函数
实现原理
利用了 JavaScript 的异步执行机制, JavaScript 会优先执行完所有的同步代码, 然后去事件队列中执行所有的异步任务
当我们不断的触发 debounced 函数时, 它会不断的 clearTimeout(timeout), 然后再重新设置新的 timeout, 所以实际上在我们的同步代码执行完之前, 每次调用 debounced 函数都会重置 timeout 所以异步事件队列中的异步任务会不断刷新, 直到最后一个 debounced 函数执行完只有最后一个 debounced 函数设置的 later 异步任务会在同步代码执行之后被执行
所以当我们在之前实验中不断的滚动鼠标时, 实际上是在不断的调用 debounced 函数, 不断的清除 timeout 对应的异步任务, 然后又设置新的 timeout 异步任务当我们停止的时间不超过 2s 时, timeout 对应的异步任务还没有被触发, 所以再次滚动鼠标触发 debounced 函数还可以清除 timeout 任务然后设置新的 timeout 任务一旦停止的时间超过 2s, 最终的 timeout 对应的异步代码就会被执行
总结
去抖是限制函数执行频率的一种方法
去抖后的函数在指定时间内最多被触发一次, 连续触发去抖后的函数只能得到一次的触发效果
underscore 去抖的实现依赖于 JavaScript 的异步执行机制, 优先执行同步代码, 然后执行事件队列中的异步代码
参考
underscore 函数去抖的实现
- JavaScript Debounce Function
- Stack Overflow:What does _.debounce do?
来源: https://juejin.im/post/5a9d3f4d6fb9a028c3684b35