前言
前面已经对防抖和节流有了介绍,这篇主要看lodash是如何将防抖和节流合并成一个函数的。
初衷是深入lodash,学习它内部的好代码并应用,同时也加深节流防抖的理解。这里会先从防抖开始一步步往后,由简入繁,直到最后实现整个函数。
这里纯粹自己的理解,以及看了很多篇优质文章,希望能加深对节流防抖的理解,如果有不同意见或者看法,欢迎大家评论。
原理
前面虽然已经介绍过防抖和节流原理,这里为了加深印象,再搬过来。
防抖的原理:在wait时间内,持续触发某个事件。第一种情况:如果某个事件触发wait秒内又触发了该事件,就应该以新的事件wait等待时间为准,wait秒后再执行此事件;第二种情况:如果某个事件触发wait秒后,未再触发该事件,则在wait秒后直接执行该事件。
通俗点说:定义wait=3000,持续点击按钮,前后点击间隔都在3秒内,则在最后一次点击按钮后,等待3秒再执行func方法。如果点击完按钮,3秒后未再次点击按钮,则3秒后直接执行func方法。
节流的原理:持续触发某事件,每隔一段时间,只执行一次。
通俗点说,3 秒内多次调用函数,但是在 3 秒间隔内只执行一次,第一次执行后 3 秒 无视后面所有的函数调用请求,也不会延长时间间隔。3 秒间隔结束后则开始执行新的函数调用请求,然后在这新的 3 秒内依旧无视后面所有的函数调用请求,以此类推。
简单来说:每隔单位时间( 3 秒),只执行一次。
代码分析
一、引入代码部分
首先看源码最前方的引入。
import isObject from './isObject.js' import root from './.internal/root.js'
isObject方法,直接拿出来,
function isObject(value) { const type = typeof value; return value != null && (type === "object" || type === "function"); }
root的引入主要是window。为了引出window.requestAnimationFrame
。
二、requestAnimationFrame代码
window.requestAnimationFrame()
告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用指定的函数来更新动画,差不多 16ms 执行一次。
lodash这里使用requestAnimationFrame
,主要是用户使用debounce函数未设置wait的情况下使用requestAnimationFrame
。
const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function') function startTimer(pendingFunc, wait) { if (useRAF) { window.cancelAnimationFrame(timerId) return window.requestAnimationFrame(pendingFunc) } return setTimeout(pendingFunc, wait) } function cancelTimer(id) { if (useRAF) { return window.cancelAnimationFrame(id) } clearTimeout(id) }
由代码const useRAF = (!wait && wait !== 0 && typeof window.requestAnimationFrame === 'function')
不难看出,函数未传入wait并且window.cancelAnimationFrame函数存在这两种情况下操作window.requestAnimationFrame
三、由简入繁输出防抖函数
-
首先,我们来看下lodash debounce API
这部分参数内容就直接摘抄在下方:- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
- [options=] (Object): 选项对象。
- [options.leading=false] (boolean): 指定在延迟开始前调用。
- [options.maxWait] (number): 设置 func 允许被延迟的最大值。
- [options.trailing=true] (boolean): 指定在延迟结束后调用。
-
然后,我们一般防抖函数,需要的参数是:
func
、wait
、immediate
这三个参数,对应lodash,我们需要拿出这四个部分:- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
- [options=] (Object): 选项对象。
- [options.leading=false] (boolean): 指定在延迟开始前调用。
-
接着,按照这个形式,先写出最简防抖方法。也就是这两部分参数的代码
- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
// 代码1 function debounce(func, wait) { let timerId, // setTimeout 生成的定时器句柄 lastThis, // 保存上一次 this lastArgs, // 保存上一次执行 debounced 的 arguments result; // 函数 func 执行后的返回值,多次触发但未满足执行 func 条件时,返回 result wait = +wait || 0; // 等待时间 // 没传 wait 时调用 window.requestAnimationFrame() const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === "function"; // 取消debounce function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastArgs = lastThis = timerId = result = undefined; } // 开启定时器 // 1.未传wait时使用requestAnimationFrame // 2.直接使用定时器 function startTimer(pendingFunc, wait) { if (useRAF) { window.cancelAnimationFrame(timerId); return window.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } // 定时器回调函数,表示定时结束后的操作 function timerExpired(wait) { const time = Date.now(); timerId = startTimer(invokeFunc, wait); } // 取消定时器 function cancelTimer(id) { if (useRAF) { return window.cancelAnimationFrame(id); } clearTimeout(id); timerId = undefined; } // 执行函数,并将原函数的返回值result输出 function invokeFunc() { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; // 清空当前函数指向的this,argumnents result = func.apply(thisArg, args); // 绑定当前函数指向的this,argumnents return result; } const debounced = function (...args) { const time = Date.now(); // 获取当前时间 lastArgs = args; lastThis = this; if (timerId) { cancelTimer(timerId); } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } }; debounced.cancel = cancel; return debounced; }
看上述代码: 1. 多了未传wait情况,使用`window.requestAnimationFrame`。 2. 将定时器,绑定this,arguments、result和取消定时器等分函数拿了出来。
- 再者,将options的leading加上。也就是immediate立即执行,组成完整的防抖函数。引入参数是下面这部分
- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
- [options=] (Object): 选项对象。
- [options.leading=false] (boolean): 指定在延迟开始前调用。
// 代码二 function debounce(func, wait, options) { let timerId, // setTimeout 生成的定时器句柄 lastThis, // 保存上一次 this lastArgs, // 保存上一次执行 debounced 的 arguments result, // 函数 func 执行后的返回值,多次触发但未满足执行 func 条件时,返回 result lastCallTime; // 上一次调用 debounce 的时间 let leading = false; // 判断是否立即执行,默认false wait = +wait || 0; // 从options中获取是否立即执行 if (isObject(options)) { leading = !!options.leading; } // 没传 wait 时调用 window.requestAnimationFrame() const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === "function"; // 取消debounce function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastArgs = lastThis = timerId = result = lastCallTime = undefined; } // 开启定时器 function startTimer(pendingFunc, wait) { if (useRAF) { window.cancelAnimationFrame(timerId); return window.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } // 定时器回调函数,表示定时结束后的操作 function timerExpired(wait) { const time = Date.now(); // 1、是否需要执行 // 执行事件结束后的那次回调,否则重启定时器 if (shouldInvoke(time)) { return trailingEdge(time); } // 2、否则 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发 timerId = startTimer(timerExpired, wait); } // 这里时触发后仍调用函数 function trailingEdge(time) { timerId = undefined; // 只有当我们有 `lastArgs` 时才调用,这意味着`func'已经被调用过一次。 if (lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } // 取消定时器 function cancelTimer(id) { if (useRAF) { return window.cancelAnimationFrame(id); } clearTimeout(id); } function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; // 清空当前函数指向的this,argumnents result = func.apply(thisArg, args); // 绑定当前函数指向的this,argumnents return result; } // 判断此时是否立即执行 func 函数 // lastCallTime === undefined 第一次调用时 // timeSinceLastCall >= wait 超过超时时间 wait,处理事件结束后的那次回调 // timeSinceLastCall < 0 当前时间 - 上次调用时间小于 0,即更改了系统时间 function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 ); } // 立即执行函数 function leadingEdge(time) { // 1、开启定时器,为了事件结束后的那次回调 timerId = startTimer(timerExpired, wait); // 1、如果配置了 leading 执行传入函数 func // leading 来源自 !!options.leading return leading ? invokeFunc(time) : result; } const debounced = function (...args) { const time = Date.now(); // 获取当前时间 const isInvoking = shouldInvoke(time); // 判断此时是否立即执行 func 函数 lastArgs = args; lastThis = this; lastCallTime = time; if (isInvoking) { // 判断是否立即执行 if (timerId === undefined) { return leadingEdge(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; }; debounced.cancel = cancel; return debounced; }
上述代码: 1. 增加trailingEdge、trailingEdge以及invokeFunc函数 2. options目前只支持传入leading参数,也就是immediate。
- 再往后,我们将options中的trailing加上,也就是这四部分
- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
- [options=] (Object): 选项对象。
- [options.leading=false] (boolean): 指定在延迟开始前调用。
- [options.trailing=true] (boolean): 指定在延迟结束后调用。
function debounce(func, wait, options) { let timerId, // setTimeout 生成的定时器句柄 lastThis, // 保存上一次 this lastArgs, // 保存上一次执行 debounced 的 arguments result, // 函数 func 执行后的返回值,多次触发但未满足执行 func 条件时,返回 result lastCallTime; // 上一次调用 debounce 的时间 let leading = false; // 判断是否立即执行,默认false let trailing = true; // 是否响应事件结束后的那次回调,即最后一次触发,false 时忽略,默认为true wait = +wait || 0; // 从options中获取是否立即执行 if (isObject(options)) { leading = !!options.leading; trailing = "trailing" in options ? !!options.trailing : trailing; } // 没传 wait 时调用 window.requestAnimationFrame() const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === "function"; // 取消debounce function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastArgs = lastThis = timerId = result = lastCallTime = undefined; } // 开启定时器 function startTimer(pendingFunc, wait) { if (useRAF) { window.cancelAnimationFrame(timerId); return window.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } // 定时器回调函数,表示定时结束后的操作 function timerExpired(wait) { const time = Date.now(); // 1、是否需要执行 // 执行事件结束后的那次回调,否则重启定时器 if (shouldInvoke(time)) { return trailingEdge(time); } // 2、否则 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发 timerId = startTimer(timerExpired, remainingWait(time)); } function remainingWait(time) { const timeSinceLastCall = time - lastCallTime; const timeWaiting = wait - timeSinceLastCall; return timeWaiting; } // 这里时触发后仍调用函数 function trailingEdge(time) { timerId = undefined; // 这意味着`func'已经被调用过一次。 if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } // 取消定时器 function cancelTimer(id) { if (useRAF) { return window.cancelAnimationFrame(id); } clearTimeout(id); } function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; // 清空当前函数指向的this,argumnents result = func.apply(thisArg, args); // 绑定当前函数指向的this,argumnents return result; } // 判断此时是否立即执行 func 函数 // lastCallTime === undefined 第一次调用时 // timeSinceLastCall >= wait 超过超时时间 wait,处理事件结束后的那次回调 // timeSinceLastCall < 0 当前时间 - 上次调用时间小于 0,即更改了系统时间 function shouldInvoke(time) { const timeSinceLastCall = time - lastCallTime; return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 ); } // 立即执行函数 function leadingEdge(time) { // 1、开启定时器,为了事件结束后的那次回调 timerId = startTimer(timerExpired, wait); // 1、如果配置了 leading 执行传入函数 func // leading 来源自 !!options.leading return leading ? invokeFunc(time) : result; } const debounced = function (...args) { const time = Date.now(); // 获取当前时间 const isInvoking = shouldInvoke(time); // 判断此时是否立即执行 func 函数 lastArgs = args; lastThis = this; lastCallTime = time; if (isInvoking) { // 判断是否立即执行 if (timerId === undefined) { return leadingEdge(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; }; debounced.cancel = cancel; return debounced; }
上述代码: 1.leading和trailing不能同时为false。
其实可以在代码中加上判断同时为false
时,默认wait=0
,直接执行window.requestAnimationFrame
部分,而不是定时器。
- 最后结合maxWait,也就是将防抖和节流合并的关键。
- func (Function): 要防抖动的函数。
- [wait=0] (number): 需要延迟的毫秒数。
- [options=] (Object): 选项对象。
- [options.leading=false] (boolean): 指定在延迟开始前调用。
- [options.maxWait] (number): 设置 func 允许被延迟的最大值。
- [options.trailing=true] (boolean): 指定在延迟结束后调用。
首先,我们可以先来看lodash throttle部分源码:
import debounce from './debounce.js' import isObject from './isObject.js function throttle(func, wait, options) { let leading = true let trailing = true if (typeof func !== 'function') { throw new TypeError('Expected a function') } if (isObject(options)) { leading = 'leading' in options ? !!options.leading : leading trailing = 'trailing' in options ? !!options.trailing : trailing } return debounce(func, wait, { leading, trailing, 'maxWait': wait }) } export default throttle
其实就是将wait传入了debounce函数的option.maxWait
中。所以最后,我们只需要将之前的代码加上maxWait参数部分。
function debounce(func, wait, options) { let timerId, // setTimeout 生成的定时器句柄 lastThis, // 保存上一次 this lastArgs, // 保存上一次执行 debounced 的 arguments result, // 函数 func 执行后的返回值,多次触发但未满足执行 func 条件时,返回 result lastCallTime, maxWait; // 上一次调用 debounce 的时间 let leading = false; // 判断是否立即执行,默认false let trailing = true; // 是否响应事件结束后的那次回调,即最后一次触发,false 时忽略,默认为true /** * 节流部分参数 **/ let lastInvokeTime = 0; // 上一次执行 func 的时间,配合 maxWait 多用于节流相关 let maxing = false; // 是否有最大等待时间,配合 maxWait 多用于节流相关 wait = +wait || 0; // 从options中获取是否立即执行 if (isObject(options)) { leading = !!options.leading; trailing = "trailing" in options ? !!options.trailing : trailing; /** * 节流部分参数 **/ maxing = "maxWait" in options; // options 中是否有 maxWait 属性,节流函数预留 maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 为设置的 maxWait 和 wait 中最大的 // 如果 maxWait < wait,那 maxWait 就没有意义了 } // 没传 wait 时调用 window.requestAnimationFrame() const useRAF = !wait && wait !== 0 && typeof window.requestAnimationFrame === "function"; // 取消debounce function cancel() { if (timerId !== undefined) { cancelTimer(timerId); } lastInvokeTime = 0; leading = false; maxing = false; trailing = true; lastArgs = lastThis = timerId = result = lastCallTime = maxWait = undefined; } // 开启定时器 function startTimer(pendingFunc, wait) { if (useRAF) { window.cancelAnimationFrame(timerId); return window.requestAnimationFrame(pendingFunc); } return setTimeout(pendingFunc, wait); } // 定时器回调函数,表示定时结束后的操作 function timerExpired(wait) { const time = Date.now(); // 1、是否需要执行 // 执行事件结束后的那次回调,否则重启定时器 if (shouldInvoke(time)) { return trailingEdge(time); } // 2、否则 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发 timerId = startTimer(timerExpired, remainingWait(time)); } // 计算仍需等待的时间 function remainingWait(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime; // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime; // 剩余等待时间 const timeWaiting = wait - timeSinceLastCall; // 是否设置了最大等待时间 // 是(节流):返回「剩余等待时间」和「距上次执行 func 的剩余等待时间」中的最小值 // 否:返回剩余等待时间 return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } // 这里时触发后仍调用函数 function trailingEdge(time) { timerId = undefined; // 这意味着`func'已经被调用过一次。 if (trailing && lastArgs) { return invokeFunc(time); } lastArgs = lastThis = undefined; return result; } // 取消定时器 function cancelTimer(id) { if (useRAF) { return window.cancelAnimationFrame(id); } clearTimeout(id); } function invokeFunc(time) { const args = lastArgs; const thisArg = lastThis; lastArgs = lastThis = undefined; // 清空当前函数指向的this,argumnents lastInvokeTime = time; result = func.apply(thisArg, args); // 绑定当前函数指向的this,argumnents return result; } // 判断此时是否立即执行 func 函数 // lastCallTime === undefined 第一次调用时 // timeSinceLastCall >= wait 超过超时时间 wait,处理事件结束后的那次回调 // timeSinceLastCall < 0 当前时间 - 上次调用时间小于 0,即更改了系统时间 // maxing && timeSinceLastInvoke >= maxWait 超过最大等待时间 function shouldInvoke(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime; // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime; // 上述 4 种情况返回 true return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait) ); } // 立即执行函数 function leadingEdge(time) { // 1、设置上一次执行 func 的时间 lastInvokeTime = time; // 2、开启定时器,为了事件结束后的那次回调 timerId = startTimer(timerExpired, wait); // 3、如果配置了 leading 执行传入函数 func // leading 来源自 !!options.leading return leading ? invokeFunc(time) : result; } const debounced = function (...args) { const time = Date.now(); // 获取当前时间 const isInvoking = shouldInvoke(time); // 判断此时是否立即执行 func 函数 lastArgs = args; lastThis = this; lastCallTime = time; if (isInvoking) { // 判断是否立即执行 if (timerId === undefined) { return leadingEdge(lastCallTime); } // 如果设置了最大等待时间,则立即执行 func // 1、开启定时器,到时间后触发 trailingEdge 这个函数。 // 2、执行 func,并返回结果 if (maxing) { // 循环定时器中处理调用 timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } } if (timerId === undefined) { timerId = startTimer(timerExpired, wait); } return result; }; debounced.cancel = cancel; return debounced; }
上述代码: 尽管代码有点长,但是实际上只是增加了maxWait。
下面我们分析下maxWait新增的那部分代码。
分析maxWait新增部分
// 1.定义变量 let maxWait; // 上一次调用 debounce 的时间 let lastInvokeTime = 0; // 上一次执行 func 的时间,配合 maxWait 多用于节流相关 let maxing = false; // 是否有最大等待时间,配合 maxWait 多用于节流相关 // 2.从options中取出maxWait if (isObject(options)) { maxing = "maxWait" in options; // options 中是否有 maxWait 属性,节流函数预留 maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 为设置的 maxWait 和 wait 中最大的 // 如果 maxWait < wait,那 maxWait 就没有意义了 } // 3.计算仍需等待的时间 function remainingWait(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime; // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime; // 剩余等待时间 const timeWaiting = wait - timeSinceLastCall; // 是否设置了最大等待时间 // 是(节流):返回「剩余等待时间」和「距上次执行 func 的剩余等待时间」中的最小值 // 否:返回剩余等待时间 return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting; } // 4.判断是否立即执行 function shouldInvoke(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime; // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime; // 上述 4 种情况返回 true return ( lastCallTime === undefined || timeSinceLastCall >= wait || timeSinceLastCall < 0 || (maxing && timeSinceLastInvoke >= maxWait) ); } // 5.有maxing时,应该如何处理函数 if (isInvoking) { // 判断是否立即执行 if (timerId === undefined) { return leadingEdge(lastCallTime); } // 如果设置了最大等待时间,则立即执行 func // 1、开启定时器,到时间后触发 trailingEdge 这个函数。 // 2、执行 func,并返回结果 if (maxing) { // 循环定时器中处理调用 timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime); } }
1.新增变量就不多说了。
2.从options中取出maxWait
:
// 2.从options中取出maxWait if (isObject(options)) { maxing = "maxWait" in options; // options 中是否有 maxWait 属性,节流函数预留 maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait; // maxWait 为设置的 maxWait 和 wait 中最大的 // 如果 maxWait < wait,那 maxWait 就没有意义了 }
- 1.这里主要是将
maxing
,判断是否传了maxWait
参数。 - 2.如果未传则
maxWait
还是为初始定义的undefined
。 - 3.如果传入了
maxWait
,则重新赋值Math.max(+options.maxWait || 0, wait)
。这里主要就是取maxWait
和wait
中的大值。
3.计算仍需等待的时间
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
首先判断是否节流(maxing):
- 是=>取「剩余等待时间」和「距上次执行 func 的剩余等待时间」中的最小值。
- 否=>取剩余等待时间
maxWait - (time - lastInvokeTime)
这里是不是就是节流中
// 下次触发 func 剩余时间 const remaining = wait - (now - previous);
4.判断是否立即执行
lodash代码:
maxing && (time - lastInvokeTime) >= maxWait
就往下执行。
这里是不是就是节流中
if (remaining <= 0 || remaining > wait)
就往下执行。
5.有maxing时,应该如何处理函数
lodash代码:如果是节流函数就执行
// 循环定时器中处理调用 timerId = startTimer(timerExpired, wait); return invokeFunc(lastCallTime);
节流函数中:
timeout = setTimeout(function () { timeout = null; previous = options.leading === false ? 0 : getNow(); // 这里是将previous重新赋值当前时间 showResult(context, args); }, remaining);
总之,lodashmaxWait
部分,尽管参数名多,但实际上就是节流函数中,判断剩余时间remaining
。不需要等待,就直接立即执行,否则就到剩余时间就执行一次,依次类推。
对外 3 个方法
debounced.cancel = cancel // 取消函数延迟执行 debounced.flush = flush // 立即执行 func debounced.pending = pending // 检查当前是否在计时中
演示地址
可以去Github仓库查看演示代码
跟着大佬学系列
主要是日常对每个进阶知识点的摸透,跟着大佬一起去深入了解JavaScript的语言艺术。
后续会一直更新,希望各位看官不要吝啬手中的赞。
❤️ 感谢各位的支持!!!
❤️ 如果有错误或者不严谨的地方,请务必给予指正,十分感谢!!!
❤️ 喜欢或者有所启发,欢迎 star!!!