我们可以想象一下:对于一个要输入信息的输入框,我们最终想要输入的肯定是在输入完成时输入框中的整个字符串去触发响应,那么对于这个过程,响应如果不加以限制我们在每次输入一个字符后都会触发响应,如果频繁的输入字符或者删除字符就会造成响应卡顿。
let Input = document.getElementsByTagName('Input')[0]
function test() {
console.log(Input.value)
}
Input.onkeydown = test
很明显,我们最终输入的是‘AAAAAAAAA’,而操作却执行了9次。这个例子很简单,但我们假设如果是一个触发频率极高的事件,并涉及到大量的计算操作相关的内容,会对服务器造成很大的压力。
防抖(debounce)和节流(throttling)是两种解决对于频繁触发DOM事件而造成响应卡顿的方案
防抖(debounce)
原理:设定一个周期去延迟执行响应操作,如果频繁的触发事件,那么会继续延期执行,直到一段时间不在触发事件才会执行响应的操作
function debounce(fn, delay = 500) {
//这里维护一个变量timer
let timer = null
return function() {
if (timer) {
//根据定时器id清空了定时器,但是timer变量存储的id值不变
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
如果频繁的去触发操作,那么每次执行匿名函数都会清空上一次执行的定时器,直到最后一次
关键是在外层函数中维护了一个timer变量,每次触发返回的匿名函数都会以外面维护的变量为参照,去判断是否需要清空定时器。每次触发返回的匿名函数都会开辟一块新的空间,他们是相互独立的,但是这里应用了闭包的思想,每次执行匿名函数都会访问同一个timer。 ·
用防抖去解决上面的例子:
//这里直接将keyup事件的回调设置为 debounce函数返回的匿名函数,这个匿名函数里面执行了test回调
Input.onkeydowm = debounce(test)
立即执行(升级版)
function debounce(fn, delay = 500) {
let timer = null
return function() {
//每次清空上一次调用时触发的定时器,timer变量存储的id值不变
if (timer) {
clearTimeout(timer)
}
//第一次调用匿名函数或者在timer被置为null后调用匿名函数都会直接调用fn
if (!timer) {
fn.apply(this, arguments)
}
//每次调用匿名函数都会触发定时器,在delay期间,timer是有id值的,所以fn不会调用
timer = setTimeout(() => {
timer = null
}, delay)
}
}
区别:
原先版本的防抖函数是延期执行fn,如果再次频繁的调用匿名函数去执行fn都会停止上一次的延期,再次重新重新设置延期,直到最后一次延期执行完毕没有再次触发事件,则会调用fn
立即执行版本的防抖函数正好相反,开始就准备执行fn,之后设置延期,如果再次频繁的调用匿名函数去执行fn同样会停止上一次的延期,再次重新设置延期,但是在每次调用匿名函数时都会尝试先去执行fn(立即执行),直到最后一次触发后,也不会有反应,延期结束。过了延期后再一次触发,立即执行,再次设置延期…
总之,原版本的防抖函数频繁触发会延期,直到最后一次触发执行操作,立即执行版本是初始第一次就立即执行,之后的频繁操作是不会有响应操作的
注意:这里说的是一个连续的过程
节流(throttling)
原理:在固定周期内只执行一次,后续再次触发则要等到过了这个周期才可以再次触发
场景:提交按钮,在固定时间(可能是没几秒)只点击一次有效
function throttling(fn, delay = 2000) {
let Do = true
return function() {
if (Do) {
fn.apply(this, arguments)
Do = false
setTimeout(() => {
Do = true
}, delay)
}
}
}
总结
防抖和节流都利用了闭包的思想,返回了一个引用外部变量的匿名函数,这个外部变量就是每次是否可以调用fn的判断依据
节流和防抖的区别在于防抖针对超高频率的触发事件会不停的延期,要么开始就触发要么最后一次触发。而节流是固定一个时间段内只可以触发一次过了这个时间段才可以再次触发。