可能80%的前端面试都遇到过这问题,也有无数人写了这方面的文章,但是我还想想写,不止是为了记录下,也是想表达下自己的想法
概念解释
首先说下节流防抖的概念问题吧
- 节流:说白了就是自定义一个时间间隔,让这个方法每隔这个时间就执行一次,其他触发这个方法的均无视。最常见的像scroll这种操作,没必要执行频率那么高,十分影响性能,搞个几百毫秒执行一次就够意思的了,当然具体执行时间间隔还得视业务需求而定
- 防抖:说白了就是你还在操作我就不执行它,等你停下来我才执行。还是拿滚动scroll来说事,你监听了文档的scroll,但希望在滚动过程中不执行定义的方法,等停下来才执行,那就是典型的防抖
在开始之前,给大家介绍下setTimeout的返回值,
返回值timeoutID是一个正整数,表示定时器的编号。这个值可以传递给clearTimeout()来取消该定时器。需要注意的是setTimeout()和setInterval(``)共用一个编号池,技术上,clearTimeout()和clearInterval()`` 可以互换。但是,为了避免混淆,不要混用取消定时函数
下面详细来说下
节流
一般来说节流有一下实现方式:
- 第一种利用闭包,最终返回个函数,先定义一个时间时间标志位
previous, 这个值为每次执行完后的时间(首次执行previous为执行时时间),当前时间与previous的差值若大于定义的间隔时间则执行,否则直接return掉
看下具体代码
常规版
function throttle(fn, wait=200) {
const self = this //暂存this,避免在返回函数里丢掉
let previous = Date.now() //存进来时候的时间
return function() { //返回新函数,这个函数内部持有对throttle函数作用域的引用,即可取到变量self以及previous的值
let now = Date.now() //取当前时间
if(now - previous > wait) { //当当前时间与previous时间差大于自定义间隔值,则执行
previous = now
fn.apply(self, Array.prototype.slice.call(arguments)) //执行,传入参数,注意arguments是伪数组,所以要转化为真数组
}else{
return
}
}
}
但有这样一个状况,就是在触发情况下,第一次执行发生在等待设定的时间后。如果我想第一次触发就执行,后面的间隔设定的时间再执行,这要怎么做呢?其实也好办,只需要把一开始开始的previous设为0,我每次判断它是否为0,为0的话就立即执行,这不就妥了吗 代码如下:
先行执行版
function throttle(fn, wait=200) {
const self = this
let previous = 0 // 一开始设为0
return function() {
const args = Array.prototype.slice.call(arguments)
if(!previous) {
fn.apply(self, args) //previous为0则立即执行
previous = Date.now() //把当前时间赋给previous,因为把当前时间赋给previous记录的总是上一次的执行时间,一开始为0除外
return
}
let now = Date.now() //取当前时间
if(now - previous > wait) {
previous = now
fn.apply(self, args)
}else{
return
}
}
}
- 第二种是利用setTimeout, 设置个timer存计时器id, 一开始timer为undefined, 以及每次执行前都把timer置为null, 以达到每次触发时判断当前有没有待执行的回调,没有才继续执行
看下代码
利用setTimeout常规版
function throttle(fn, wait=200) {
const self = this //暂存this,避免在返回函数里丢掉
let timer //暂存定时器ID
return function() {
const args = Array.prototype.slice.call(arguments) //参数
if(!timer) { //若当前没有待执行的定时器回调
timer = setTimeout(function() {
timer = null //这步尤为重要,执行了回调,就把timer置空,证明当前没有待执行的定时器回调了
fn.apply(self, args) //主函数执行
}, wait)
}
}
}
同常规版同理,若想第一次触发就执行,只需判断
function throttle(fn, wait=200) {
...
if(!timer) {
fn.apply(self, args) //只需要加这句
timer = setTimeout(function() {
...
}
}
防抖
如果理解了节流,防抖也是比较容易理解的。简单来说就是如果在归档的时间间隔内执行函数,会重新触发计时。
普通版
function debounce(fun, delay) {
let timer;
return function (args) {
let self = this
if(timer) { //存在定时器,则清掉
clearTimeout(timer)
}
timer = setTimeout(function () {
fun.apply(self, Array.prototype.slice.call(arguments))
}, delay)
}
}
同样的,以上代码同样存在这样一种情况,就是只有连续触发,停止触发后等待设定的时间后回调才会执行。如果我想第一次触发就执行,后面连续触发都不执行,停止后等待待设定的时间后回调才会执行。
其实我只需要在一开始加个是否存在定时器id便可判断这是否为第一次触发。
先触发版
如下:
function debounce(fun, delay) {
let timer;
let self = this
return function (args) {
const args = Array.prototype.slice.call(arguments)
!timer && fun.apply(self, args) //重点是这句,
if(timer) { //存在定时器,则清掉
clearTimeout(timer)
}
timer = setTimeout(function () {
fun.apply(self,args)
}, delay)
}
}
以上的代码大家都可以copy下面的去验证下,记得测试throttle要换下下面对应的函数名
function con() {
console.log(222)
}
document.addEventListener('scroll', debounce(con, 200))
总结
可能代码放得有点多,但大家只要记住节流跟防抖的目的,实现的方式有多种,也可以继续扩展,例如配置第一次触发是否立即执行,最后一次触发是否一定执行,也可配置取消节流跟防抖的监听。方法多种多样,把基本的弄懂,其他怎么搞还不是易如反掌,对吧聪明的小伙伴们
本文使用 mdnice 排版