防抖和节流都是对多次频繁触发的处理,但是二者又有很大不同。

    防抖是将多次触发情况变为一次触发,如下图蓝色的圆,准备从左边走到右边,而每次点击按钮都会让其重新回到左边。我们把左到右这段距离比作防抖限制的时间wait,那么每次触发,只要wait时间没有走完,则 wait 会重新开始计时。

    debounce-ball.gif

    节流则是将多次触发变为每隔一段时间触发一次。如下图红色的球,每次触发,只要还没走完 wait 时间,就不会从头开始走。

    throttle-ball.gif

    对于防抖函数而言,我们要设置一个 timer ,作为每次触发时的标杆。

    如下面的流程图所示,当 timer 存在,说明上一次的 wait 时间没有走完,需要将 timer 清空,重新计时。如果 timer 不存在,就赋值一个 timer 开始计时,再判断函数是否要立即执行,是的话则执行代码,清空 timer ,不是的话即开始等待 wait 时间的结束。如果 wait 时间还未结束,函数又被触发了,则回到顶部的函数调用,再进行一轮判断。

    防抖函数流程图.jpg
    节流函数则在函数调用时判断是否走完了 wait ,是则执行,不是则继续等待。
    节流函数流程图.jpg
    本篇小文主要是帮自己梳理防抖和节流的不同,以及其中的实现思路。学习过程里一直参看 JS | 前端进阶之道 。为了方便查阅,特将其中的防抖函数实现代码贴在下方。

    1. // 这个是用来获取当前时间戳的
    2. function now() {
    3. return +new Date()
    4. }
    5. /**
    6. * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
    7. *
    8. * @param {function} func 回调函数
    9. * @param {number} wait 表示时间窗口的间隔
    10. * @param {boolean} immediate 设置为ture时,是否立即调用函数
    11. * @return {function} 返回客户调用函数
    12. */
    13. function debounce (func, wait = 50, immediate = true) {
    14. let timer, context, args
    15. // 延迟执行函数
    16. const later = () => setTimeout(() => {
    17. // 延迟函数执行完毕,清空缓存的定时器序号
    18. timer = null
    19. // 延迟执行的情况下,函数会在延迟函数中执行
    20. // 使用到之前缓存的参数和上下文
    21. if (!immediate) {
    22. func.apply(context, args)
    23. context = args = null
    24. }
    25. }, wait)
    26. // 这里返回的函数是每次实际调用的函数
    27. return function(...params) {
    28. // 如果没有创建延迟执行函数(later),就创建一个
    29. if (!timer) {
    30. timer = later()
    31. // 如果是立即执行,调用函数
    32. // 否则缓存参数和调用上下文
    33. if (immediate) {
    34. func.apply(this, params)
    35. } else {
    36. context = this
    37. args = params
    38. }
    39. // 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
    40. // 这样做延迟函数会重新计时
    41. } else {
    42. clearTimeout(timer)
    43. timer = later()
    44. }
    45. }
    46. }

    节流函数代码也是借鉴 JS | 前端进阶之道,不过只取了部分以实现功能。这里对wait是否走完的判断依据为当前时间和上一次调用时间的差值:

    1. function throttle (func, wait) {
    2. var context, args
    3. // 设置前一个函数被触发的时间戳
    4. var previous = 0
    5. // 返回给用户调用的回调
    6. return function () {
    7. var now = new Date().getTime()
    8. // 首次进入
    9. if (!previous) previous = now
    10. // 准备context和args
    11. context = this
    12. args = arguments
    13. // 计算剩余的时间时长
    14. var remaining = wait - (now - previous)
    15. // 如果 now 超过了previous + wait,可以执行函数
    16. if (remaining <= 0) {
    17. previous = now
    18. timeout = null
    19. result = func.apply(context, args)
    20. context = args = null}
    21. }
    22. }

    之前学习节流时,做过一个小练习,也贴在此,若有兴趣,可以看看效果,持续点击+按钮即可。


    以上内容是 2020.03.20 在语雀发的,现在 2021.01.18 ,我再来看时,发现是无法默写或者自己实现防抖方法,甚至一些逻辑也忘了。虽然我很认真地学了防抖甚至还写了一篇颇费力气的文章分享,最终的结果却是过不了多久就忘了。

    这也是我对自己做事情怀疑的一点,每当想写些什么东西时就会想到,你写了就代表理解了吗,代表你掌握了吗,代表你拥有分享的能力了吗?是不是你在用这种输出的方式来掩盖真正的思考,毕竟即使文章写得再漂亮,你实现不出来,这不是飘在空中的知识吗?

    所以我又反思了一遍,为什么即使写了文章,也还实现不了。首先我画了示意图,流程图,企图用直观的表现形式来解释防抖的概念,事实是虽然记不得,但我一阅读之前的内容,就轻易回想起了防抖的逻辑,而不是刚学习时还要拼命地去理解,联想。输出有效果,这个不能否认。

    那为什么我还是无法实现这二十几行代码呢?我想,虽然自己理解了业务逻辑,但没想明白代码逻辑,我画了流程图,以为自己已经理解了代码逻辑,但实际上不是,事实上这个流程图都是根据已有的代码而画的,又怎么能说根据流程图就能实现代码呢,这关系不倒了吗?

    真正的代码逻辑应该是如何拆分业务逻辑的一个个 dots ,然后根据语言特有的特性结合起来,去用代码来将这些 dots 连接。

    现在来复现一下考虑实现防抖函数的思路。

    首先,在业务代码中,我调用了一个经防抖 debounce 函数处理后的函数,传入了参数 params ,那么 debounce 函数返回值应是个函数(下文以 anonymous 代替),在这个函数中要判断函数触发时间的间隔是否达到了要求,也就是本次调用的时间离上一次调用的时间是否有 wait 时长,那么 anonymous 中会有 if…else… 语块。

    真正要思考的点来了,在判断语句中,要以什么条件判断。这个条件得能告知两次触发的间隔是否走过 wait ,如果没有现成的代码,我是想不到用定时函数来做的。一般的想法可能是通过变量保留并比较两次时间点的长度。而定时函数的时间特性帮我们省了这些事,因为定时函数只要开始调用,就会计时,直到下次触发时就可以判断计时是否完毕,从而得出是否达到了时间间隔。也就是计时的开始和结束代替了记录两个时间点的变量。

    然后就是定时函数需要定时处理什么,要执行原方法,清空变量和定时器,这样自然而然的,防抖函数整体拼接完毕,代码实现完毕。

    这种经验让自己怀疑,我总在逃避学习和事情里最核心的,最难的,也是最有益的那部分。浮躁的心喜欢全而大,在脑子里勾勒幻想,营造虚假的获得感,而且自己充满怀疑,在做事和思考时会怀疑意义,有时都分不清是在真正的思考还是找借口逃避。

    然而事实是,干想怎么也想不出所以然,不如直接去做,做的过程答案自己就会浮现了。