跳到主要内容

函数的节流和防抖

节流

函数节流:Throttling enforces a maximum number of times a function can be called over time. As in "execute this function at most once every 100 milliseconds".

其他解释:函数节流指的是某个函数在一定时间间隔内(例如 3 秒)只执行第一次,在这 3 秒内无视后来产生的函数调用请求,也不会延长时间间隔。

基础实现

function throttle(func, delay) {
let timer = null;
return function (...args) {
if (!timer) {
func.apply(this, args);
timer = setTimeout(() => {
clearTimeout(timer);
}, delay);
}
};
}

const betterFn = throttle(function (...args) {
console.log('hello world', ...args);
}, 3000);

// 模拟scroll事件触发,并传入参数
let fNode = document.querySelector('.father');
fNode.onscroll = function () {
// 获取元素内部总被卷去的内容的横向距离和纵向距离
console.log('x: ' + fNode.scrollLeft, ', y: ' + fNode.scrollTop);
betterFn([1, 2, 3, 4]);
};
<!-- index.html -->
<style>
* {
margin: 0;
padding: 0;
}
.father {
width: 300px;
height: 300px;
background-color: #3de;
margin: 100px auto;
padding: 10px;
overflow: auto;
border: 10px solid red;
}
.son {
width: 400px;
height: 600px;
background-color: yellowgreen;
}
</style>
</head>

<body>
<div class="father"><div class="son"></div></div>
<script src="./index.js"></script>
</body>

升级

var throttleV3 = function (fn, delay, mustRunDelay, isFirstRun) {
var timer = null;
var t_start;
return function () {
var context = this,
args = arguments,
t_curr = +new Date();
clearTimeout(timer);
if (!t_start) {
t_start = t_curr;
}
// 控制第一次是否跑
if (isFirstRun) {
fn.apply(context, args);
}
if (t_curr - t_start >= mustRunDelay) {
fn.apply(context, args);
t_start = t_curr;
} else {
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
}
};
};

// window.onresize = function(){ throttleV2(myFunc, 100, 300, true); }

防抖(debounce)

函数防抖:Debouncing enforces that a function not be called again until a certain amount of time has passed without it being called. As in "execute this function only if 100 milliseconds have passed without it being called".

防抖函数:指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次。

// html样式同节流
const debounce = function (fn, wait = 500) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, wait);
};
};

// 调用防抖
const betterFn = debounce(function (...args) {
// your code ...
console.log('fn 防抖执行了', ...args);
}, 1000);

// 模拟scroll事件触发,并传入参数
let fNode = document.querySelector('.father');
fNode.onscroll = function () {
console.log('x: ' + fNode.scrollLeft, ', y: ' + fNode.scrollTop);
};

加强版 throttle

function throttle(fn, wait) {
// previous 是上一次执行 fn 的时间
// timer 是定时器
let previous = 0,
timer = null;

// 将 throttle 处理结果当作函数返回
return function (...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date();

// ------ 新增部分 start ------
// 判断上次触发的时间和本次触发的时间差是否小于时间间隔
if (now - previous < wait) {
// 如果小于,则为本次触发操作设立一个新的定时器
// 定时器时间结束后执行函数 fn
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
previous = now;
fn.apply(this, args);
}, wait);
// ------ 新增部分 end ------
} else {
// 第一次执行 或 时间间隔超出了设定的时间间隔,执行函数 fn
previous = now;
fn.apply(this, args);
}
};
}

// 第一次触发 scroll 执行一次 fn,每隔 1 秒后执行一次函数 fn,停止滑动 1 秒后再执行函数 fn
document.addEventListener(
'scroll',
throttle(() => console.log('fn 节流执行了'), 1000)
);

lodash 的防抖

/**
* @category功能
* @param{Function}func要去抖动的函数。
* @param{number}〔wait=0〕要延迟的毫秒数。
* @param{Object}[options={}]选项对象。
* @param{boolean}[选项.前导=false]
* 指定在超时的前沿调用。
* @param{number}[选项.maxWait]
* 调用`func`之前允许延迟的最长时间。
* @param{boolean}[选项.tracing=真]
* 指定在超时的后沿调用。
* @returns{Function}返回新的去抖动函数。
*
* @param {Function} func 要去抖动的函数。
* @param {number} [wait=0] 要延迟的毫秒数。
* @param {Object} [options={}] 选项对象。
* @param {boolean} [options.leading=false] 指定在超时的前沿调用。
* @param {number} [options.maxWait] 调用`func`之前允许延迟的最长时间。
* @param {boolean} [options.trailing=true] 指定在超时的后沿调用。
* @returns {Function} 返回新的去抖动函数。
*/
function debounce(func, wait, options) {
var lastArgs, // 保存最后一次调用时的参数
lastThis, // 保存最后一次调用时的上下文
maxWait, // 最大等待时间
result, // 保存最后一次函数调用的结果
timerId, // 计时器的 ID
lastCallTime, // 最后一次调用的时间
lastInvokeTime = 0, // 最后一次执行函数的时间
leading = false, // 是否在开始时调用函数
maxing = false, // 是否启用最大等待时间
trailing = true; // 是否在结束后调用函数

// 检查 `func` 是否为函数
if (typeof func != 'function') {
throw new TypeError('Expected a function');
}

// 如果未指定 `wait`,则默认为 0
wait = wait || 0;

// 检查 `options` 是否为对象,并设置相应的标志
if (isObject(options)) {
leading = !!options.leading;
maxing = 'maxWait' in options;
maxWait = maxing ? Math.max(options.maxWait || 0, wait) : maxWait;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}

// 执行函数并更新相关状态
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

// 处理 `leading` 选项,调用函数并启动计时器
function leadingEdge(time) {
// 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 ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting;
}

// 判断是否应该调用函数
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;

// 要么这是第一次通话,活动已经停止,我们处于
// 后缘,系统时间倒退了,我们正在处理
// 否则我们将达到“最大等待”极限。
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}

// 计时器过期时调用的函数
function timerExpired() {
var time = Date.now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}

// 处理 `trailing` 选项,调用函数
function trailingEdge(time) {
timerId = undefined;

// 只有当我们有`lastArgs`时才调用,这意味着`func`已经
// 至少被撤销过一次。
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(Date.now());
}

function debounced() {
var time = Date.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;
}

function isObject(value) {
var type = typeof value;
return value != null && (type == 'object' || type == 'function');
}

// 防抖函数示例
function logMessage() {
console.log('Debounced function called');
}

var debouncedLog = debounce(logMessage, 1000, { leading: true, trailing: false });

window.addEventListener('resize', debouncedLog);

ts 中的防抖

// 实现一个防抖函数, 该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法

type Options = {
/** 先调用后等待, 默认为 false (即先等待后调用) */
leading?: boolean;
};

type Features = {
/** 取消延迟的函数调用 */
cancel: () => void;
/** 立即调用 */
flush: () => void;
};

type DebounceFunction<T extends (...args: any[]) => any> = ((
...args: Parameters<T>
) => ReturnType<T>) &
Features;

function debounce<T extends (...args: any[]) => any>(
fn: T,
wait: number,
options: Options = {}
): DebounceFunction<T> {
let timeoutId: ReturnType<typeof setTimeout> | null;
let lastArgs: Parameters<T> | null;
let lastThis: ThisParameterType<T> | null;
let result: ReturnType<T>;
let lastCallTime: number | null = null;

const { leading = false } = options;

const invokeFunction = (time: number) => {
lastCallTime = time;
result = fn.apply(lastThis, lastArgs as Parameters<T>);
lastArgs = lastThis = null;
return result;
};

const leadingEdge = (time: number) => {
lastCallTime = time;
timeoutId = setTimeout(timerExpired, wait);
return leading ? invokeFunction(time) : result;
};

const remainingWait = (time: number) => {
const timeSinceLastCall = time - (lastCallTime as number);
return wait - timeSinceLastCall;
};

const timerExpired = () => {
const time = Date.now();
if (lastArgs) {
timeoutId = setTimeout(timerExpired, remainingWait(time));
return invokeFunction(time);
}
timeoutId = null;
};

const debounced = function (this: ThisParameterType<T>, ...args: Parameters<T>) {
const time = Date.now();
const isInvoking = leading && timeoutId === null;

lastArgs = args;
lastThis = this;

if (isInvoking) {
return leadingEdge(time);
}

if (timeoutId === null) {
timeoutId = setTimeout(timerExpired, wait);
}

return result;
} as DebounceFunction<T>;

debounced.cancel = () => {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = lastArgs = lastThis = lastCallTime = null;
};

debounced.flush = () => {
if (timeoutId !== null && lastArgs) {
invokeFunction(Date.now());
clearTimeout(timeoutId);
timeoutId = lastArgs = lastThis = lastCallTime = null;
}
};

return debounced;
}

export default debounce;