防抖和节流都是做性能优化的方案,它们功能相近,经常会被一起提及,但却又有不同的适用场景。
防抖
概念及手写
所谓的防抖,其实是函数的一种特性,可以描述为:
一个具有防抖特性的函数被触发时,它将等待特定时间后"执行",但如果在等待期间被再次触发,则重新计时。
我们平常说的"防抖函数",通常指的是"能返回具有防抖特性函数的函数"。也就是说所谓的防抖函数是一个高阶函数,它本身并不具有防抖的特性,而是经过它包装之后,所返回的函数具有防抖特性。
为了实现在等待时间内重复调用函数时,会重新计时的特点,需要借助闭包的特点,在父级函数中定义变量,使返回出去的闭包函数在每次运行时,都能访问到同一个变量(再去做相应的逻辑判断),而不是重新生成局部变量(这样无法在多次调用时共享状态)。
具体实现如下所示:
javascript
function debounce(fn, delay) {
// 在父级函数中定义,使返回的闭包函数在多次调用时,能访问到同一个变量
let timer = null;
return function () {
// 每次执行函数时,清除上一次可能存在的延时器
timer && clearTimeout(timer);
const args = arguments;
// 函数每次被触发时,都重新开始计时
timer = setTimeout(() => {
// this绑定为闭包函数运行时的this指向
fn.apply(this, args);
}, delay);
};
}
上述代码能保证函数 fn 的 this 能正确绑定,原因如下:
箭头函数被声明的时间是在 setTimeout 执行的瞬间(或者说前一刻),此刻箭头函数的 this 指向会继承上层作用域的 this 指向,也就是返回出去的闭包函数的 this 指向。所以防抖函数返回出去的"真正的防抖函数"是可以按照需求绑定 this 指向的。
应用场景
在某些快速触发事件的时候,希望在整个频繁触发的过程中都不运行函数,只在最后一次触发后运行的情况,如:
表单的校验:当用户快速输入内容时,在整个输入期间都不对内容进行校验,只在停止输入后进行一次校验。
节流
概念及手写
同样的,节流也是函数的一种特性,它的特点可描述为:
一个具有节流特性的函数被触发时,它会立即执行并且开始倒计时,在倒计时结束前无法再次被触发运行。
节流函数的实现同样需要借助闭包的特性,以及注意 this 的绑定,具体代码如下所示:
javascript
function throttle(fn, delay) {
// 在父级函数中定义,使返回的闭包函数在多次调用时,能访问到同一个变量
let timer = null;
return function () {
// 如果timer不存在,则代表倒计时结束了
if (timer === null) {
fn.apply(this, arguments);
// 开始本次倒计时
timer = setTimeout(() => {
// 倒计时结束,重置标志位
timer = null;
}, delay);
}
};
}
应用场景
在某些快速触发事件的时候,希望减少函数真正运行的次数的情况,如:
搜索栏输入框:当用户在输入框快速输入时,每隔特定时间展示新的建议项,可减少请求次数,并且在整个输入期间都能展现建议项。
测试用例
以下是完整代码及测试用例,展示防抖及节流的特点,以及正确的传参及 this 绑定。
javascript
function debounce(fn, delay) {
// 在父级函数中定义,使返回的闭包函数在多次调用时,能访问到同一个变量
let timer = null;
return function () {
// 每次执行函数时,清除上一次可能存在的延时器
timer && clearTimeout(timer);
const args = arguments;
// 函数每次被触发时,都重新开始计时
timer = setTimeout(() => {
// this绑定为闭包函数运行时的this指向
fn.apply(this, args);
}, delay);
};
}
function throttle(fn, delay) {
// 在父级函数中定义,使返回的闭包函数在多次调用时,能访问到同一个变量
let timer = null;
return function () {
// 如果timer不存在,则代表倒计时结束了
if (timer === null) {
fn.apply(this, arguments);
// 开始本次倒计时
timer = setTimeout(() => {
// 倒计时结束,重置标志位
timer = null;
}, delay);
}
};
}
function print(type, x, y, z) {
console.log(type + '打印:', x, y, z, this.text)
}
// 用于测试this指向是否绑定成功
const obj = {
text: 'hello world'
}
const dePrint = debounce(print, 1000)
const throPrint = throttle(print, 1000)
// 防抖函数测试函数
function testDebounce() {
let count = 0
let timer = setInterval(() => {
dePrint.apply(obj, ['防抖', 1,2,3])
count++
// 每隔500毫秒尝试触发一次,共尝试4次
if (count === 4) clearInterval(timer)
}, 500)
}
// 节流函数测试函数
function testThrottle() {
let count = 0
let timer = setInterval(() => {
throPrint.apply(obj, ['节流', 1,2,3])
count++
// 每隔500毫秒尝试触发一次,共尝试4次
if (count === 4) clearInterval(timer)
}, 500)
}
testDebounce() // 最终只会在最后一次运行dePrint
testThrottle() // 最终会成功运行2次