防抖和节流都是对多次频繁触发的处理,但是二者又有很大不同。
防抖是将多次触发情况变为一次触发,如下图蓝色的圆,准备从左边走到右边,而每次点击按钮都会让其重新回到左边。我们把左到右这段距离比作防抖限制的时间wait,那么每次触发,只要wait时间没有走完,则 wait 会重新开始计时。
节流则是将多次触发变为每隔一段时间触发一次。如下图红色的球,每次触发,只要还没走完 wait 时间,就不会从头开始走。
对于防抖函数而言,我们要设置一个 timer ,作为每次触发时的标杆。
如下面的流程图所示,当 timer 存在,说明上一次的 wait 时间没有走完,需要将 timer 清空,重新计时。如果 timer 不存在,就赋值一个 timer 开始计时,再判断函数是否要立即执行,是的话则执行代码,清空 timer ,不是的话即开始等待 wait 时间的结束。如果 wait 时间还未结束,函数又被触发了,则回到顶部的函数调用,再进行一轮判断。
节流函数则在函数调用时判断是否走完了 wait ,是则执行,不是则继续等待。
本篇小文主要是帮自己梳理防抖和节流的不同,以及其中的实现思路。学习过程里一直参看 JS | 前端进阶之道 。为了方便查阅,特将其中的防抖函数实现代码贴在下方。
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later()
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
节流函数代码也是借鉴 JS | 前端进阶之道,不过只取了部分以实现功能。这里对wait是否走完的判断依据为当前时间和上一次调用时间的差值:
function throttle (func, wait) {
var context, args
// 设置前一个函数被触发的时间戳
var previous = 0
// 返回给用户调用的回调
return function () {
var now = new Date().getTime()
// 首次进入
if (!previous) previous = now
// 准备context和args
context = this
args = arguments
// 计算剩余的时间时长
var remaining = wait - (now - previous)
// 如果 now 超过了previous + wait,可以执行函数
if (remaining <= 0) {
previous = now
timeout = null
result = func.apply(context, args)
context = args = null}
}
}
之前学习节流时,做过一个小练习,也贴在此,若有兴趣,可以看看效果,持续点击+按钮即可。
以上内容是 2020.03.20 在语雀发的,现在 2021.01.18 ,我再来看时,发现是无法默写或者自己实现防抖方法,甚至一些逻辑也忘了。虽然我很认真地学了防抖甚至还写了一篇颇费力气的文章分享,最终的结果却是过不了多久就忘了。
这也是我对自己做事情怀疑的一点,每当想写些什么东西时就会想到,你写了就代表理解了吗,代表你掌握了吗,代表你拥有分享的能力了吗?是不是你在用这种输出的方式来掩盖真正的思考,毕竟即使文章写得再漂亮,你实现不出来,这不是飘在空中的知识吗?
所以我又反思了一遍,为什么即使写了文章,也还实现不了。首先我画了示意图,流程图,企图用直观的表现形式来解释防抖的概念,事实是虽然记不得,但我一阅读之前的内容,就轻易回想起了防抖的逻辑,而不是刚学习时还要拼命地去理解,联想。输出有效果,这个不能否认。
那为什么我还是无法实现这二十几行代码呢?我想,虽然自己理解了业务逻辑,但没想明白代码逻辑,我画了流程图,以为自己已经理解了代码逻辑,但实际上不是,事实上这个流程图都是根据已有的代码而画的,又怎么能说根据流程图就能实现代码呢,这关系不倒了吗?
真正的代码逻辑应该是如何拆分业务逻辑的一个个 dots ,然后根据语言特有的特性结合起来,去用代码来将这些 dots 连接。
现在来复现一下考虑实现防抖函数的思路。
首先,在业务代码中,我调用了一个经防抖 debounce 函数处理后的函数,传入了参数 params ,那么 debounce 函数返回值应是个函数(下文以 anonymous 代替),在这个函数中要判断函数触发时间的间隔是否达到了要求,也就是本次调用的时间离上一次调用的时间是否有 wait 时长,那么 anonymous 中会有 if…else… 语块。
真正要思考的点来了,在判断语句中,要以什么条件判断。这个条件得能告知两次触发的间隔是否走过 wait ,如果没有现成的代码,我是想不到用定时函数来做的。一般的想法可能是通过变量保留并比较两次时间点的长度。而定时函数的时间特性帮我们省了这些事,因为定时函数只要开始调用,就会计时,直到下次触发时就可以判断计时是否完毕,从而得出是否达到了时间间隔。也就是计时的开始和结束代替了记录两个时间点的变量。
然后就是定时函数需要定时处理什么,要执行原方法,清空变量和定时器,这样自然而然的,防抖函数整体拼接完毕,代码实现完毕。
这种经验让自己怀疑,我总在逃避学习和事情里最核心的,最难的,也是最有益的那部分。浮躁的心喜欢全而大,在脑子里勾勒幻想,营造虚假的获得感,而且自己充满怀疑,在做事和思考时会怀疑意义,有时都分不清是在真正的思考还是找借口逃避。
然而事实是,干想怎么也想不出所以然,不如直接去做,做的过程答案自己就会浮现了。