“节流”与“防抖”的本质

1,029 阅读4分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

这两个东西都以闭包的形式存在。

它们通过对事件对应的回调函数进行包裹、以自由变量的形式缓存时间信息,最后用 setTimeout 来控制事件的触发频率。

一. 节流 Throttle: 第一个人说了算

节流,即节省交互沟通。流,可理解为交流,不一定会产生网络流量。

image.png 例如,drag 的回调,上传进度的回调,都可以设置一个固定的频率,没必要那么频繁。

throttle 的中心思想在于:在某段时间内,不管你触发了多少次回调,我都只认第一次,并在计时结束时给予响应。

先给大家讲个小故事:现在有一个旅客刚下了飞机,需要用车,于是打电话叫了该机场唯一的一辆机场大巴来接。司机开到机场,心想来都来了,多接几个人一起走吧,这样这趟才跑得值——我等个十分钟看看。于是司机一边打开了计时器,一边招呼后面的客人陆陆续续上车。在这十分钟内,后面下飞机的乘客都只能乘这一辆大巴,十分钟过去后,不管后面还有多少没挤上车的乘客,这班车都必须发走。

在这个故事里,“司机” 就是我们的节流阀,他控制发车的时机;“乘客”就是因为我们频繁操作事件而不断涌入的回调任务,它需要接受“司机”的安排;而“计时器”,就是我们上文提到的以自由变量形式存在的时间信息,它是“司机”决定发车的依据;最后“发车”这个动作,就对应到回调函数的执行。

总结下来,所谓的“节流”,是通过在一段时间内无视后来产生的回调请求来实现的。只要一位客人叫了车,司机就会为他开启计时器,一定的时间内,后面需要乘车的客人都得排队上这一辆车,谁也无法叫到更多的车。

对应到实际的交互上是一样一样的:每当用户触发了一次 scroll 事件,我们就为这个触发操作开启计时器。一段时间内,后续所有的 scroll 事件都会被当作“一辆车的乘客”——它们无法触发新的 scroll 回调。直到“一段时间”到了,第一次触发的 scroll 事件对应的回调才会执行,而“一段时间内”触发的后续的 scroll 回调都会被节流阀无视掉。

1.1 手写节流 Throttle

<body>
    <p>throttle</p>
    <div id="div1" draggable="true" style="width: 100px; height: 50px; background-color: #ccc; padding: 10px;">
        可拖拽
    </div>

    <script>
        function throttle(fn, delay = 100) {
            let timer = 0

            return function () {
                if (timer) return

                timer = setTimeout(() => {
                    fn.apply(this, arguments)
                    timer = 0
                }, delay)
            }
        }

        const div1 = document.getElementById('div1')
        div1.addEventListener('drag', throttle((e) => {
            console.log('鼠标的位置', e.offsetX, e.offsetY)
        }))
    </script>
</body>

1.2 应用场景

  • 鼠标不断点击触发,mousedown(单位时间内只触发一次)
  • 监听滚动事件,比如是否滑到底部自动加载更多,用throttle来判断

二. Debounce: 最后一个人说了算

防抖,即防止抖动。抖动着就先不管它,等啥时候静止了,再做操作。

在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

例如,一个搜索输入框,等输入停止之后,自动执行搜索。

image.png

debounce 的问题在于它“太有耐心了”。

试想,如果用户的操作十分频繁——他每次都不等 debounce 设置的 delay 时间结束就进行下一次操作,于是每次 debounce 都为该用户重新生成定时器,回调函数被延迟了不计其数次。

频繁的延迟会导致用户迟迟得不到响应,用户同样会产生“这个页面卡死了”的观感。

为了避免弄巧成拙,我们需要借力 throttle 的思想,打造一个“有底线”的 debounce——等你可以,但我有我的原则:delay 时间内,我可以为你重新生成定时器;但只要delay的时间到了,我必须要给用户一个响应。

这个 throttle 与 debounce “合体”思路,已经被很多成熟的前端库应用到了它们的加强版 throttle 函数的实现中

2.1 手写防抖 Debounce

<body>
    <p>debounce</p>
    搜索 <input id="input1">
    
    <script>
        function debounce(fn, delay = 200) {
            let timer = 0

            return function () {
                if (timer) clearTimeout(timer)

                timer = setTimeout(() => {
                    fn.apply(this, arguments) // 透传 this 和参数
                    timer = 0
                }, delay)
            }
        }

        const input1 = document.getElementById('input1')
        input1.addEventListener('keyup', debounce(() => {
            console.log('发起搜索', input1.value)
        }), 300)
    </script>
</body>

2.2 应用场景

  • search搜索联想,用户在不断输入值时,用防抖来节约请求资源。
  • window触发resize的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

三、防抖和节流写法区别

 function throttle(fn, delay = 100) {
    let timer = 0

    return function () {
        // 节流直接 return
        if (timer) return

        timer = setTimeout(() => {
            fn.apply(this, arguments)
            timer = 0
        }, delay)
    }
}

function debounce(fn, delay = 200) {
    let timer = 0

    return function () {
            // 防抖 清楚定时器,重新计时
        if (timer) clearTimeout(timer)

        timer = setTimeout(() => {
            fn.apply(this, arguments) // 透传 this 和参数
            timer = 0
        }, delay)
    }
}

四、总结

  • 节流: 限制执行频率,有节奏的执行
  • 防抖:限制执行次数,多次密集的处罚只执行最后一次

节流关注“过程”

防抖关注“结果”

实际工作中可以使用 lodash www.lodashjs.com