防抖 debounce
防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐!
简单代码实现
1 2 3 4 5 6 7
| function debounce(fn, wait) { var timer = null; return function() { clearTimeout(timer); timer = setTimeout(fn, wait); }; }
|
这只是简单的实现函数防抖,存在很多问题和局限性。这次来分析一下 lodash 中的 debounce 实现。
存在问题
在看 lodash 源码之前先来分析一下简单实现存在的问题
this 指向问题
上面的代码 debounce 返回的函数 this 指向了 Window 对象。在实际需求中我们希望 this 指向正确的对象(一般为触发事件的 DOM 元素)。
1 2 3 4 5 6 7 8 9 10 11 12
| function debounce(func, wait) { var timeout;
return function() { var context = this;
clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context); }, wait); }; }
|
event 对象
我们使用 debounce 方法包装之后,我们会丢失原来事件绑定的 event 对象,我们需要把他找回来。
1 2 3 4 5 6 7 8 9 10 11 12
| function debounce(func, wait) { var timeout; return function() { var context = this; var args = arguments;
clearTimeout(timeout); timeout = setTimeout(function() { func.apply(context, args); }, wait); }; }
|
立即执行问题
上面的版本解决了 this 指向和 event 对象的问题,但是在实际使用中我们会有一个很常见的需求,我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。我们再来改造原来的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function debounce(func, wait, leading) { var timeout;
return function() { var context = this; var args = arguments;
if (timeout) clearTimeout(timeout); if (leading) { var callNow = !timeout; timeout = setTimeout(function() { timeout = null; }, wait); if (callNow) func.apply(context, args); } else { timeout = setTimeout(function() { func.apply(context, args); }, wait); } }; }
|
lodash 的 debounce 实现
上面的代码已经比较完善了,下面来看下 lodash 中的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| function debounce(func, wait, options) { let lastArgs, lastThis, maxWait, result, timerId, lastCallTime;
let lastInvokeTime = 0; let leading = false; let maxing = false; let trailing = true;
if (typeof func != "function") { throw new TypeError("Expected a function"); } wait = +wait || 0; if (isObject(options)) { }
function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis;
lastArgs = lastThis = undefined; lastInvokeTime = time; result = func.apply(thisArg, args); return result; }
function leadingEdge(time) { lastInvokeTime = time; timerId = setTimeout(timerExpired, wait); return leading ? invokeFunc(time) : result; }
function remainingWait(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime; const timeWaiting = wait - timeSinceLastCall;
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; }
function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; const timeSinceLastInvoke = time - lastInvokeTime;
return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait) ); }
function timerExpired() { const time = Date.now(); if (shouldInvoke(time)) { return trailingEdge(time); } timerId = setTimeout(timerExpired, remainingWait(time)); }
function trailingEdge(time) { timerId = undefined;
if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; }
function cancel() {}
function flush() {}
function pending() {}
function debounced(...args) { const time = Date.now(); const isInvoking = shouldInvoke(time);
lastArgs = args; lastThis = this; lastCallTime = time;
if (isInvoking) { if (timerId === undefined) { return leadingEdge(lastCallTime); } if (maxing) { timerId = setTimeout(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = setTimeout(timerExpired, wait); } return result; } debounced.cancel = cancel; debounced.flush = flush; debounced.pending = pending; return debounced; }
|
lodash 中的优化
maxWait
参数保证超过一定时间保证调用一次函数
trailing
参数保证延迟结束后调用一次函数
- 加了取消(cancel)、刷新(flush)、暂停(pending) 防抖的方法。
- 兼容了被包装函数有返回值的情况
具体使用可以查看官方中文文档