防抖 / Debounce
Debounce,防抖函数是一种优化函数调用频率的手段,它能在规定的时间内只让函数执行一次。这对于一些需要频繁触发但又不希望频繁执行的场景非常有用,比如输入框的输入监听、窗口大小的变化监听等。
示例代码
function debounce(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}function debounce(fn: (...args: any[]) => void, delay: number): (...args: any[]) => void {
let timer: NodeJS.Timeout | null = null;
return function(...args: any[]): void {
const context = this;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}参数与变量
fn: 这是你希望被防抖的函数,也就是你希望在一段时间内只执行一次的函数。delay: 这是一个时间间隔,单位是毫秒。即你希望在触发行为停止后多久,fn函数才被真正执行。比如,如果delay是 1000,那么只有在最后一次触发fn后的 1 秒钟内没有再次触发,fn函数才会被执行。timer: 这是一个用于存储定时器的变量。每次触发防抖函数,都会清除之前的定时器,并重新设置一个新的定时器。context和args: 这两个变量用于在setTimeout的回调函数中,保持fn函数的调用上下文和参数。context保存了fn函数的this值,args保存了传给fn函数的参数。
工作原理
- 当防抖函数被触发时,先清除之前的定时器(如果存在的话),然后设置一个新的定时器。新的定时器会在
delay毫秒后执行。 - 如果在
delay毫秒内,防抖函数再次被触发,那么就会清除之前的定时器,再设置一个新的定时器。这样一来,只要防抖函数在delay毫秒内被连续触发,fn函数就不会被执行。 - 只有在最后一次触发防抖函数后的
delay毫秒内没有再次触发,定时器才会到时间,从而执行fn函数。
这个防抖函数的实现使用了 JavaScript 的闭包和定时器功能。闭包是指函数有访问到它自己被定义时的词法作用域,即使在它被调用时已经在不同的作用域了。在这个防抖函数中,返回的匿名函数就形成了一个闭包,它可以访问并修改它被定义时的作用域中的 timer 变量。每次调用 debounce 函数,都会创建一个新的作用域,以及一个新的 timer 变量。然后,返回的函数通过闭包,可以访问并修改这个 timer 变量。这就是为什么每个通过 debounce 函数生成的函数(比如下面 例子 1 中的 onChange1、onChange5 和 onChange10 )都有各自的定时器,它们之间是互不影响的。
例子 1
function debounce(fn, delay) {
let timer = null;
return function() {
const context = this;
const args = arguments;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
}
// 定义一个被防抖的函数
function log() {
console.log('防抖函数执行');
}
// 使用debounce函数包装原函数,设置不同的防抖延迟
let debouncedLog1 = debounce(log, 1000);
let debouncedLog5 = debounce(log, 5000);
let debouncedLog10 = debounce(log, 10000);
// 交错调用三个函数
debouncedLog1(); // 不会立即执行 log,而是等待 1 秒
debouncedLog5(); // 不会立即执行 log,而是等待 5 秒
debouncedLog1(); // 上一次的等待被清除,重新开始等待 1 秒
debouncedLog10(); // 不会立即执行 log,而是等待 10 秒
debouncedLog5(); // 上一次的等待被清除,重新开始等待 5 秒
debouncedLog1(); // 上一次的等待被清除,重新开始等待 1 秒
// 1 秒后,没有新的 debouncedLog1 被调用,log 函数执行
// 5 秒后,没有新的 debouncedLog5 被调用,log 函数执行
// 10 秒后,没有新的 debouncedLog10 被调用,log 函数执行节流 / Throttling
在 Web 开发中,节流函数(Throttling)也是一种重要的优化手段,它可以限制函数的执行频率,避免在短时间内过于频繁地执行一些操作,从而提高性能和用户体验。
示例代码
/**
* Throttle Function: Limits the execution rate of a function.
* @param {Function} func - Function to be throttled.
* @param {Number} wait - Time delay in milliseconds.
* @returns {Function} - Throttled function.
*/
function throttle(func, wait) {
let lastExecTime, timeoutId;
return function(...args) {
const context = this;
const now = Date.now();
if (lastExecTime && now < lastExecTime + wait) {
// If the function is invoked in less than 'wait' time, reset the timer.
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
lastExecTime = now;
func.apply(context, args);
}, wait);
} else {
// Else, execute the function and set the last execution time.
lastExecTime = now;
func.apply(context, args);
}
};
}/**
* Throttle Function: Limits the execution rate of a function.
* @param {Function} func - Function to be throttled.
* @param {Number} wait - Time delay in milliseconds.
* @returns {Function} - Throttled function.
*/
function throttle(func, wait) {
let lastExecTime, timeoutId;
return function(...args) {
const context = this;
const now = Date.now();
if (lastExecTime && now < lastExecTime + wait) {
// If the function is invoked in less than 'wait' time, reset the timer.
clearTimeout(timeoutId);
timeoutId = setTimeout(function() {
timeoutId = setTimeout(() =>{
lastExecTime = now;
func.apply(context, args);
func(...args);
}, wait);
} else {
// Else, execute the function and set the last execution time.
lastExecTime = now;
func.apply(context, args);
func(...args);
}
};
}/**
* Throttle Function: Limits the execution rate of a function.
* @param {() => void} func - Function to be throttled.
* @param {number} wait - Time delay in milliseconds.
* @returns {(...args: any[]) => void} - Throttled function.
*/
const throttle = (func: (...args: any[]) => void, wait: number): ((...args: any[]) => void) => {
let lastExecTime: number | null = null;
let timeoutId: NodeJS.Timeout;
return function(...args: any[]): void {
const now: number = Date.now();
if (lastExecTime && now < lastExecTime + wait) {
// If the function is invoked in less than 'wait' time, reset the timer.
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
lastExecTime = now;
func(...args);
}, wait);
} else {
// Else, execute the function and set the last execution time.
lastExecTime = now;
func(...args);
}
};
}参数与变量
func:是需要被节流的函数。wait:是设定的延迟执行时间。lastExecTime:用来记录上次函数执行的时间。timeoutId:用来存储定时器的 ID,用于清除定时器。...args:是传递给被节流函数的参数列表。context:是函数执行的上下文,这里是使用了this关键字来获取。now:是当前时间,用于和lastExecTime对比,判断是否已经过了wait时间。setTimeout:是用来设定延迟执行的函数,如果在wait时间内再次调用,会清除之前的定时器,并重新设定一个新的定时器。clearTimeout:是用来清除定时器的,避免函数的重复执行。func.apply(context, args):是用来执行函数的,使用了apply方法来指定函数的执行上下文和参数。
工作原理
- 当调用
throttle函数时,会返回一个新的函数。这个新函数在被调用时,会检查当前时间(now)与上次执行时间(lastExecTime)的差值是否小于设定的延迟时间(wait)。 - 如果是(即在
wait时间内再次调用了函数),则清除已设定的定时器(clearTimeout(timeoutId))并重新设定一个新的定时器(setTimeout)。这个新的定时器会在wait时间后执行原函数,并更新lastExecTime。 - 如果不是(即已过了
wait时间),则立即执行原函数,并更新lastExecTime。 - 无论是立即执行还是延迟执行,函数的执行都是通过
func.apply(context, args)实现的,其中context是函数执行的上下文,args是函数调用时传入的参数。
这样,通过 throttle 函数,我们就可以控制函数的执行频率,让其在一定时间内只执行一次。这对于一些需要频繁触发但不需要频繁响应的事件(如滚动、拖拽、窗口大小改变等)非常有用,能有效提高性能。
对比
说了这么多,这俩都是让函数在一定时间内只运行一次,那是否存在区别呢?答案是,区别还是有的!
防抖(debounce)是在一段时间间隔结束后,执行一次函数,通常是在这段时间的 最后一次调用。如果在这段时间间隔内再次触发了函数调用,那么会重新计算时间间隔。防抖常用于搜索框/滚动事件,用户在停止输入或滚动一段时间后,才去执行一次处理。
节流(throttle)则是在一段时间间隔内,只允许函数执行一次,通常是这段时间的 开始时刻。如果在这段时间间隔内再次触发了函数调用,那么这些调用都会被忽略。节流常用于滚动加载,用户滚动时只处理一次滚动事件,无论这段时间内触发了多少次滚动。
所以,防抖和节流虽然都是为了限制函数的执行频率,但是它们的应用场景和实现方式都有所不同。
其他
this
在 JavaScript 中,函数的 this 上下文是在调用时确定的,而不是在定义时确定的。这会导致在某些情况下,函数的 this 上下文不是我们期望的那个。例如,当我们将一个对象的方法作为回调函数传递给另一个函数时,这个方法的 this 上下文可能会丢失,变成全局对象(在非严格模式下)或 undefined(在严格模式下)。
为了解决这个问题,我们可以使用 Function.prototype.apply 或 Function.prototype.bind 方法显式地设置函数的 this 上下文。在上面的 JavaScript 版本的节流函数中,我们就是用 Function.prototype.apply 方法来确保函数的 this 上下文正确。
然而,在 TypeScript(以及 ES6+)中,我们可以使用箭头函数来绑定 this 上下文。箭头函数不会创建自己的 this 上下文,而是会从定义它的上下文中继承 this。因此,当我们在箭头函数中使用 this 关键字时,它总是指向定义箭头函数的上下文。
在上面的 TypeScript 版本的节流函数中,我们用箭头函数替代了 Function.prototype.apply 方法,因此不需要显式地设置函数的 this 上下文。这是 TypeScript(以及 ES6+)的一个优点,它使得代码更简洁、更易于理解。