Lodash之debounce
了解 Throttling(防抖) 和 Debouncing(节流)
参考:
The Difference Between Throttling and Debouncing
Debouncing and Throttling Explained Through Examples
应⽤场景:⼀个典型的应⽤场景是浏览器窗⼝中 scrolling 和 resizing,如设置了滚动的监听函数,在滚动 5000px的时候可能会触发 100 次以上的监听事件,如果监听事件做了⼤量计算或操作很多 DOM 元素,可能就会遇到性能问题。即时搜索也有同样的问题。
相同点:它们是为了解决性能问题⽽限制基于 DOM 事件的 JavaScript 的执⾏次数的两种⽅式,这是在事件和函数执⾏之间加的控制,因为DOM 事件的触发频率是⽆法控制的。
不同点:Throttling 是限制⼀个函数能够被执⾏的最⼤时间间隔,保证了函数⾄少每隔 X 毫秒会被调⽤⼀次,如每隔 100ms 执⾏⼀次函数。Debouncing 是限制⼀个函数距上次调⽤达到⼀定时间间隔才会被再次调⽤,相当于连续的事件被分成了⼀组,只触发⼀次函数调⽤,如距上次调⽤达到 100ms 才会再次执⾏。
了解 requestAnimationFrame
requestAnimationFrame 的优势,在于充分利⽤显⽰器的刷新机制,⽐较节省系统资源。显⽰器有固定的刷新频率(60Hz 或
75Hz),requestAnimationFrame 的基本思想就是与这个刷新频率保持同步,利⽤这个刷新频率进⾏页⾯重绘。此外,使⽤这个API,⼀旦页⾯不处于浏览器的当前标签,就会⾃动停⽌刷新,这就节省了CPU、GPU和电⼒。注意 requestAnimationFrame 是在主线程上完成,这也意味着,如果主线程⾮常繁忙,requestAnimationFrame 的动画效果会⼤打折扣。santa claus
requestAnimationFrame 是限制函数执⾏次数的另⼀种⽅式,可以被认为是 _.throttle(dosomething, 16),但是是⾼保真的,会针对不同设备本⾝的性能⽽更精确⼀些,浏览器内部决定渲染的最佳时机,它可以作为 throttle 的替换。
如果浏览器标签不是激活状态,就不会被执⾏,虽然对滚动、⿏标或键盘事件没有影响。还有需要考虑浏览器兼容性,node.js 中也没有提供该API。
最佳实践:使⽤ requestAnimationFrame 进⾏重新绘制、计算元素位置或直接改变属性的操作,使⽤ _.debounce 或 _.throttle 进⾏ Ajax 请求或添加、移除 class(可以触发 CSS 动画),这时可以设置⼀个低⼀些的频率,如 200ms。
lodash 之 debounce 源码
这⾥不再描述 throttle 了,其实 throttle 就是设置了 maxWait 的 debounce,lodash 源码中对 throttle 的实现就是调⽤了 wait 和maxWait 相等的 debounce。
/*
* root 为全局变量,浏览器下为 window,node.js 下为 global
* isObject 函数判断传⼊参数是否为⼀个对象
* 创建⼀个 debounced 函数并返回,该函数延迟 func 在距离上⼀次调⽤达到 wait 时间之后再执⾏,如果在这期间内⼜调⽤了函数则将取消前⼀次并重新计* 算时间
* options.leading 函数在每个等待延迟的开始被调⽤
* ailing 函数在每个等待延迟的结束被调⽤
* options.maxWait 函数被调⽤的最⼤等待时间,实现 throttle 效果,保证⼤于⼀定时间后⼀定能执⾏
* 如果 leading 和 trailing 都设置为 true 了,只有函数在 wait 时间内被执⾏⼀次以上才会执⾏ trailing
*/
function debounce(func, wait, options) {
// 变量初始化
let lastArgs,
lastThis,
maxWait,
result,
result,
timerId,
lastCallTime;
let lastInvokeTime = 0;
let leading = fal;
let maxing = fal;
let trailing = true;剖面图英文
// 如果 wait = NaN 并且当前是浏览器环境 requestAnimationFrame 存在时,返回 true
const urRAF = (!wait && wait !== 0 && questAnimationFrame === 'function');
// 传⼊参数的验证
if (typeof func != 'function') {
throw new TypeError('Expected a function');
}
福州雅思培训班
wait = +wait || 0; // 将传⼊的 wait 转为数字,如果没有传⼊值默认赋值为 0
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
// maxWait 为设置的 maxWait 值和 wait 值中最⼤的,因为如果 maxWait ⼩于 wait,debounce 就失效了,相当于只有 throttle 了 maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!ailing : trailing;
}
function invokeFunc(time) {
// 进⼊ debounced 函数时对 lastArgs、lastThis 进⾏的赋值,在这⾥执⾏完函数后,对 lastArgs、lastThis 进⾏了重置
/
/ 个⼈认为这样做的原因,是保证通过计时的⽅式执⾏函数最多只能执⾏⼀次
const args = lastArgs;
const thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function startTimer(pendingFunc, wait) {
gj什么意思if (urRAF) {
questAnimationFrame(pendingFunc);
}
return tTimeout(pendingFunc, wait);
}
function cancelTimer(id) {
if (urRAF) {
return root.cancelAnimationFrame(id);
}
clearTimeout(id);
implicit}
intenfunction leadingEdge(time) {
// TODO 不明⽩为什么这⾥需要更新 lastInvokeTime,进⼊ leadingEdge 函数不⼀定会真的触发函数的执⾏
lastInvokeTime = time;
// 为 trailingEdge 触发函数调⽤设置定时器
timerId = startTimer(timerExpired, wait);
// 如果 leading 为 true,会触发函数执⾏,否则返回上⼀次执⾏结果
return leading ? invokeFunc(time) : result;
考会计证需要什么书}
// 主要作⽤就是触发 trailingEdge
function timerExpired() {
const time = w();
golfstar// 在 trailingEdge 且时间符合条件时,调⽤ trailingEdge函数,否则重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// 重启定时器,保证下⼀次时延的末尾触发
timerId = startTimer(timerExpired, remainingWait(time));
}
function remainingWait(time) {
// 距离上次函数被调⽤的时间
const timeSinceLastCall = time - lastCallTime;
// 距离上次函数被执⾏的时间
const timeSinceLastInvoke = time - lastInvokeTime;
const timeSinceLastInvoke = time - lastInvokeTime;
/
/ wait - timeSinceLastCall 为距离下⼀次 trailing 的位置
const timeWaiting = wait - timeSinceLastCall;
// maxWait - timeSinceLastInvoke 为距离下⼀次 maxing 的位置
// 有maxing:⽐较出下⼀次 maxing 和下⼀次 trailing 的最⼩值,作为下⼀次函数要执⾏的时间
// ⽆maxing:在下⼀次 trailing 时执⾏ timerExpired
在线英汉互译器
return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
}
function trailingEdge(time) {
timerId = undefined;
// 有 lastArgs 才执⾏,意味着只有 func 已经被 debounced 过⼀次,也就是被调⽤过⼀次,以后才会在 trailingEdge 执⾏
if (trailing && lastArgs) {
return invokeFunc(time);
}
// 每次 trailingEdge 都会清除 lastArgs 和 lastThis,⽬的是避免最后⼀次函数被执⾏了两次
// 举个例⼦:最后⼀次函数执⾏的时候,可能恰巧是前⼀次的 trailing edge,函数被调⽤,⽽这个函数⼜需要在⾃⼰时延的 trailing edge 触发,导致触发多次 lastArgs = lastThis = undefined;
return result;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
return (
lastCallTime === undefined // 第⼀次调⽤
|| (timeSinceLastCall >= wait) // 距离上次被调⽤已经超过 wait
|| (timeSinceLastCall < 0) //系统时间倒退
|| (maxing && timeSinceLastInvoke >= maxWait) //超过最⼤等待时间
);
}
// 取消函数延迟执⾏
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
// 触发函数⽴即执⾏
function flush() {
// 如果前⾯没有定时任务在执⾏,也就是没有前⾯没有调⽤过函数,返回最后⼀次执⾏的结果,否则才会触发⼀次函数执⾏
return timerId === undefined ? result : w());
}
// 检查当前是否在计时中
function pending() {
return timerId !== undefined;
}
// 返回的控制函数真正调⽤频率的函数
普林斯顿大学排名
function debounced(...args) {
const time = w();
const isInvoking = shouldInvoke(time);
lastArgs = args;
lastThis = this;
// 更新上次函数调⽤时间
lastCallTime = time;
// ⽆ timerId 的情况有两种:1.⾸次调⽤ 2.trailingEdge执⾏过函数
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait);
return invokeFunc(lastCallTime);