- 本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
- 这是源码共读的第25期,链接:juejin.cn/post/708744…。
防抖(debounce)
在第一次触发事件时,不立即执行函数,而是给出一个期限值比如200ms,然后:
- 如果在200ms内没有再次触发滚动事件,那么就执行函数
- 如果在200ms内再次触发滚动事件,那么当前的计时取消,重新开始计时
效果:如果短时间内大量触发同一事件,只会执行一次函数。
案例
// 手写的防抖函数
// 参数:func要防抖的函数, wait等待的毫秒数, immediate是否立刻执行
function debounce(func, wait, immediate) {
let timerId, result;
function debounced() {
let context = this; // 保存上下文
var args = arguments;
if(timerId) clearTimeout(timerId);
if (immediate) { //立刻执行
let callNow = !timerId; // 如果已经执行过,不再执行
timerId = setTimeout(function () {
timerId = null;
if (!callNow) result = func.apply(context, args);
}, wait);
if (callNow) result = func.apply(context, args);
} else {
timerId = setTimeout(() => {
result = func.apply(context, args);
timerId = null;
}, wait);
}
return result; // 返回 func 函数的返回值
}
debounced.cancel = function () {
clearTimeout(timerId);
timerId = null;
};
return debounced;
}
// 处理函数
function handle() {
console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle, 1000));
underscore.debounce
用法
_.debounce(function, wait, [immediate])
var lazyLayout = _.debounce(calculateLayout, 300);
$(window).resize(lazyLayout);
// 手动取消
lazyLayout.cancel()
源码
import restArguments from './restArguments.js';
import now from './now.js';
export default function debounce(func, wait, immediate) {
var timeout, previous, args, result, context;
// 定时器计时结束后
// 如果设定时间 > 过去的时间,重置定时器,时间为wait - passed
// 否则:
// 1、清空计时器,使之不影响下次连续事件的触发
// 2、触发执行 func
var later = function() {
var passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
if (!immediate) result = func.apply(context, args);
// This check is needed because `func` can recursively invoke `debounced`.
if (!timeout) args = context = null;
}
};
// 执行restArguments,返回函数
var debounced = restArguments(function(_args) {
context = this;
args = _args;
previous = now();
if (!timeout) {
timeout = setTimeout(later, wait); // 设置定时器
if (immediate) result = func.apply(context, args); // 立刻执行
}
return result;
});
// 手动取消
debounced.cancel = function() {
clearTimeout(timeout);
timeout = args = context = null;
};
return debounced;
}
lodash.debounce
用法
_.debounce(func, [wait=0], [options={}])
案例:
// Avoid costly calculations while the window size is in flux.
jQuery(window).on('resize', _.debounce(calculateLayout, 150));
// Invoke `sendMail` when clicked, debouncing subsequent calls.
jQuery(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
// Ensure `batchLog` is invoked once after 1 second of debounced calls.
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
jQuery(source).on('message', debounced);
// Cancel the trailing debounced invocation.
jQuery(window).on('popstate', debounced.cancel);
源码
参数:
- func **(Function) **: 要防抖动的函数。
- [wait=0] **(number) **: 需要延迟的毫秒数。
- [options={}] **(Object) **: 选项对象。
- [options.leading=false] **(boolean) **: 指定在延迟开始前调用。
- [options.maxWait] **(number) **: 设置 func 允许被延迟的最大值。
- [options.trailing=true] **(boolean) **: 指定在延迟结束后调用。
function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
function invokeFunc(time) { // 调用func
var args = lastArgs,
thisArg = lastThis;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}
function leadingEdge(time) { // 启动 setTimeout
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}
function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) { // 判断是否调用
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
function timerExpired() { // 判断是否应该调用,是则调用trailingEdge,否则重启定时器
var time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);
lastArgs = arguments;
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
节流(throttle)
当持续触发事件时,保证一定时间段内只调用一次事件处理函数。
- 定时器方式
function throttle(fn,delay){
let valid = true
return function() {
if(!valid){
//休息时间 暂不接客
return false
}
// 工作时间,执行函数并且在间隔期内把状态位设为无效
valid = false
setTimeout(() => {
fn()
valid = true;
}, delay)
}
}
function handle() {
console.log(Math.random());
}
window.addEventListener('scroll', throttle(handle, 1000));
- 时间戳方式
function throttle(fn, delay) {
let last = 0 // 这样能保证第一次触发能够立即被执行
return function throttle_fn() {
if(Date.now() - last >= delay) {
fn.apply(this, arguments)
last = Date.now()
}
}
}
第一次在掘金发文,如有错误,请大佬们指正啦!本文节流部分写的比较简单,后续会更新。