开始
防抖节流是前端性能优化的一种手段之一,它们的目的都是防止某一事件一段时间内频发触发,但是两者的原理不一样。
根据上图,我们也可以很清晰地看到他们的原理,总结如下:
- 防抖:事件触发 n 秒后执行它的回调函数,如果 n 秒内重新触发,那么重新计时。
- 节流: n 秒内只执行一次事件
防抖
- 基于上述的定义,我们可以很快地写出第一版的代码
const debounce = function (fn, delay = 3000) {
let timer = null // 闭包引用
return function () {
const args = arguments // 拷贝回调函数参数
if (timer) {
clearInterval(timer) // 清除定时器
timer = null
}
// 每次都重新计时
timer = setTimeout(() => {// 箭头函数解决 this 指向问题,不然指向的是 window 或者 global
return fn.apply(this, args)
}, delay)
}
}
- 很多场景下,上面这一版本已经够用了。比如说文本框的 change 监听,或者是 window.resize 的事件监听,我们关注的只是最后一次触发。
但难免有时候会有些特殊的需求,比如文本框第一次触发就想去请求后端数据,然后再进行后面的防抖过程,基于此我们又有了第二版的代码。
var debounce = function (fn, delay = 3000, immediate = false) {
let timer = null
return function () {
const args = arguments, context = this
if (timer) {
clearTimeout(timer)
}
if (immediate) { // 要么是立即执行
let isCall = !timer // 判断当前是否能执行
timer = setTimeout(() => {
timer = null
}, delay)
isCall && fn.apply(context, args)
} else { // 要么就是正常流程
timer = setTimeout(() => {
fn.apply(context, args)
}, delay)
}
}
}
节流
根据节流的定义,我们可以使用两种方法来实现,第一种是时间戳,第二种是定时器
// 基于时间戳的版本的头调用
const throttle = function (fn, delay) {
let previous = 0
return function () {
let now = +new Date() // 转换为时间戳
if (now - previous > delay) {// 如果满足当前时间- 上一次完成的时间 > 等待时间
fn(arguments)
previous = +new Date()
}
}
}
// 基于定时器实现的尾调用
const throttle = function (fn, delay) {
let timer = null
return function () {
const context = this, args = arguments
if (timer) { return } // 如果当前有定时任务,跳过
timer = setTimeout(() => {
fn.apply(context, args)
timer = null
}, delay)
}
}
- 时间戳的版本
第一次事件触发就能运行(previous 为 0),但是如果中途停止触发的话那就退出了- 定时器版本
等待 n 秒后第一次触发,如果中途退出的话仍然会执行最后一次(此时的timer 还在,定时任务还在)
- 对比了两个版本,我们发现各有各的特点,那么有一种想法能不能把这两种特点联合起来呢,基于此就可以迭代出第三版了。
/*
难点在于当定时器和时间戳两者发生冲突的时候选择哪一个?
@example
{
delay = 1s
0s 时间戳立即执行,定时器同时设置 1s 回调
1s 的时候时间戳可以执行,定时器也可以执行
}
*/
const throttle = function (fn, delay) {
let timer = null, previous = 0
return function () {
const context = this, args = arguments
let now = +new Date()
let remaining = delay - (now - previous)
if (remaining <= 0) {
// 这里选择的是时间戳的优先级更高,可根据上面的 example进行理解
clearTimeout(timer)
timer = null
fn.apply(context, args)
previous = +new Date() // 更新时间戳
} else if(!timer){
timer = setTimeout(() => {
previous = +new Date()
fn.apply(this, args)
timer = null
}, remaining)
}
}
}