函数防抖和节流其实是很相似的概念,都是为了优化减少高频率事件对浏览器性能的影响。比如,在进行窗口的 resize 或者 scroll 时,或者在对输入框的输入内容进行校验等操作时,如果这些监听事件处理函数调用的频率无限制,等会加重浏览器的负担,最后可能会影响到用户的体验。
举一个例子,比如输入框监听键盘事件,然后进行一些操作:
<Input onKeyUp={this.handleOriginChange}/>
handleOriginChange = (event) => {
this.ajax(event.target.value)
}
ajax = (value) => {
console.log(`ajax value: ${value}.`)
}
看一下运行结果:
可以发现,只要我们按下键盘就会触发一些操作,过频繁的操作会造成资源的浪费。所以,为了减轻浏览器的负担,我们从减少处理函数调用频率而不影响体验出发,有了防抖和节流两种方式。
防抖
防抖就是说,我们设置一个时间间隔 delayTime 秒,在事件触发之后计时器延迟 delayTime 秒再执行事件的响应函数,如果在这个 delayTime 秒期间内再次触发了该事件,则之前的计时器清除,再重新设置计时器延迟 delayTime 秒之后执行响应函数。因此能够看出防抖函数有一个特性,就是只执行最后一次。
我们可以用电梯来举例,当我们在做电梯的时候,需要等最后一个人进来了之后等待10秒(不主动按关门键时)再关门,如果期间又有人进来,电梯门还需要重新等待10秒才能关门。
基本实现
使用计时器来简单的实现防抖函数:
/**
* 函数防抖
* @param {Function} fn 需要防抖的函数
* @param {Number} delayTime 时间间隔毫秒数
*/
const debounce = (fn, delayTime = 100) => {
let timer = null
return function(...args) {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, args)
}, delayTime)
}
}
对上面的输入框监听键盘事件进行防抖处理,结果运行如下:
我们用计时器来模拟用户的频繁操作。
function hello() {
console.log(`hello`, new Date().getSeconds())
}
function world() {
console.log(`world`, new Date().getSeconds())
}
setInterval(debounce(hello, 500), 1000)
setInterval(debounce(world, 2000), 1000)
可以看到运行结果如下:
与普通函数不同的是,第一次执行 hello 函数时是等待了 1.5s 之后输出了 hello 38,之后每隔1s,执行一遍 hello 函数。然后执行 world 函数的时间间隔 1000 在防抖的时间间隔 2000 内,所以每次还没执行就刷新了计时器,导致 world 函数一直没有执行。其实这就反映出来了我们写的基本防抖函数存在的两个问题,一个是首次函数不执行,另一个是如果执行时间间隔一直在防抖时间间隔内的话,则函数一直不执行。
使用场景
比如输入框中输入一段文字停止一段时间再去获取数据,窗口的 resize 事件等。
节流
函数节流可以简单的理解为某个函数在一定的时间间隔 betweenTime 内只执行一次,第一次会立即执行,但是在 betweenTime 期间内的函数调用都被忽略,betweenTime 之后的第一次会立即执行,依次类推。因此可以看出节流的一个特性,就是执行第一次。
这个可以用拧紧的水龙头来理解,水滴一段时间只滴一次,水龙头就是控制水滴流速的节流阀。
基本实现
节流有三种实现方式,计时器方式,时间戳方式,计时器和时间戳方式。
计时器实现方式
计时器的话,函数第一次调用立即执行,并且设置一个时长 betweenTime 的计时器,在 betweenTime 时间内调用函数,则忽略不执行,计时器结束之后清除计时器。再调用函数时,如果计时器清除了则立即执行,并同时设置计时器,然后依此类推。
/**
* 函数节流-计时器方式
* @param {Function} fn 需要节流的函数
* @param {Number} betweenTime 时间间隔毫秒数
*/
function throttle(fn, betweenTime) {
let timer = null
return function (...args) {
if (!timer) {
fn.apply(this, args)
timer = setTimeout(() => {
timer = null
}, betweenTime)
}
}
}
对上面的输入框监听事件使用节流,运行结果如下:
时间戳实现方式
使用时间戳的话,就需要全局保留上一次执行函数的时间了,然后拿上一次执行函数的时间与现在调用的时间比,如果差值小于 betweenTime 则忽略,如果差值大于 betweenTime 则执行。如果上次执行函数的时间是空,则说明是第一次,则立即执行,然后记录执行时间,再调用函数的时候,拿上一次执行函数的时间与调用的时间比,然后依此类推。
/**
* 函数节流-时间戳方式
* @param {Function} fn 需要节流的函数
* @param {Number} betweenTime 时间间隔毫秒数
*/
function throttle(fn, betweenTime) {
let previous = 0
return function (...args) {
const now = +new Date()
if (now - previous > betweenTime) {
fn.apply(this, args)
previous = now
}
}
}
计时器+时间戳实现方式
上面的两种实现方式都存在一个问题,就是最后一次调用没有执行,比如上面的输入框最后输入的字符没有打印出来。所以我们可以使用计时器和时间戳两种搭配的方式来实现当第一次触发的时候就执行函数,最后一次触发的时候也执行函数。
单看红色的那次事件触发,延迟 delayTime 执行,就很像防抖的概念。防抖的特性就是只执行最后一次,所以我们可以在节流的概念中加入防抖来实现最后一次触发的时候也执行函数。
/**
* 函数节流-时间戳+计时器方式
* @param {Function} fn 需要节流的函数
* @param {Number} betweenTime 时间间隔毫秒数
*/
function throttle(fn, betweenTime) {
let timer = null
let preview = 0
return function (...args) {
const now = +new Date()
clearTimeout(timer)
if (now - preview > betweenTime) {
fn.apply(this, args)
preview = now
} else {
timer = setTimeout(() => {
fn.apply(this, args)
}, betweenTime)
}
}
}
使用场景
比如监听滚动事件,判断是否滚动到底部加载更多,比如鼠标不断点击触发事件等。