2

浅谈throttle以及debounce的原理和实现


背景

日常开发中,我们经常会遇到一些需要节流调用,或者压缩调用次数的情况,例如之前我在完成一个需求的时候,就遇到了因为后端并发问题,导致收到多条socket信息从而导致函数被重复调用的情况,当时的做法是通过setTimeout对函数的调用进行注册,遇到多次调用的时候,清空前一次的调用,以后一次为准.后来在阅读underscore源码的时候,发现这种做法与debounce以及throttle的实现不谋而合.因而简单记录一下.

throttle与debounce

throttledebounce在用于控制函数的多次调用的时候,非常的有效.throttle函数能够控制目标函数在一定的时间内最多只会调用一次.而debounce函数,则可以压缩调用的次数,把多次函数调用压缩成只调用一次(多次的函数调用之间的间隔不能超过规定的时间间隔).
这样文字描述起来可能比较难理解,不过不要紧,当初我在看underscore源码的时候,也是非常难以理解这两个函数的需求,感觉好像都差不多一样.而且underscore1.1.3版本中,采用了相同的底层实现,只是接口传入的参数不同而已.因而我们这里采用一个坐电梯的例子来说明.

debounce电梯

假设你正在准备乘坐电梯,并且电梯门准备关上然后上升的时候,你的同事来了,出于礼貌,我们需要停止电梯的关闭,让同事进入.假设源源不断的有同事进来的话,电梯就需要处于一种待机的状态,一直等待人员的进入,直到没有新的同事进入或者说电梯满了,这个时候,电梯才能运行.另外,同事的进入需要在电梯门的关闭之前,否则的话,就只能等下一趟了.
换成图示我们可以这么理解

debounce

上面一排方块为函数的调用,下面的方块则是函数实际的运行.我们可以看到,即使函数多次调用,在短暂的暂停后,函数只会运行一次.

debounce运用场景

既然debounce函数可以把多次的函数调用压缩成一次,那么我们在进行Markdown渲染的时候,就可以排上用场了.如果我们在每一次键盘的敲击都进行一次Markdown渲染,必然会造成部分的计算冗余,同时也可能因为多次无畏的渲染导致页面卡顿,影响体验,因而我们可以使用debounce函数,把Markdown的渲染进行压缩,只在键盘敲击结束了一定的时间后((可以完成一次词语或者语句的输入),再进行渲染,能够减少许多冗余的计算,提高体验.

throttle电梯

throttle电梯不想debounce电梯一样会无限的等待,而是我们设定一个时间,例如10s,那么10s内,其他的人可以不断的进入电梯,但是,一旦10s过去了,那么无论如何,电梯都会进入运行的状态.

换成图示,我们可以这么理解

throttle
上面一排的方块是函数的调用,我们可以看到,及时进行了多次的函数调用,函数也只会在隔一段时间实际运行一次,不会每一次的函数调用都运行

throttle运用场景

throttle也有另外一个称号,就是节流函数,顾名思义就是能够节省函数调用时的资源消耗,达到防止系统资源被一直大量占用,从而影响其他函数执行的情况.throttle一个运用的比较广泛的场景则是通过对scroll函数进行节流,因为每一次滚动页面,都有进行资源的消耗计算,但是完全没必要每一次滚动时间触发的时候,都进行计算,这样有可能会导致大量的计算堆积而出现跳帧的情况发生,因而我们需要使用throttle函数进行节流,在滚动事件发生了一段事件后,再统一的进行处理,只要时间设置的合理,用户一般是感知不到的.

debounce与throttle的原理与实现

解释的再多,也不如我们直接自己实现一遍debouncethrottle,这样对于两个函数的运用和理解,都会更上一层楼.debouncethrottle在许多的库,例如jQuery,loadash以及underscore中都有实现,这里采用underscore1.1.3版本的实现,非常简单而且能够达到目的(其实主要是最近在看underscore源码)
代码如下

    // throttle 和 debouce 函数的底层实现
    var limit = function(func, wait, debounce) {
        var timeout;
        return function() {
            var context = this, args = arguments;
            // 封装函数,用于延迟调用
            var throttler = function() {
                // 只是节流函数的时候,对其timeout进行赋值为null,这样可以设置下一次的setTimtout
                timeout = null;
                func.apply(context, args);
            };
            // 如果debouce是true的话,前一个函数的调用timeout会被清空,不会被执行
            // 就是debounce函数的调用,这个前一个函数的不会执行.下面会重新设定setTimeout用于
            // 执行这一次的调用.
            // 但是如果是throttle函数,则会执行前一个函数的调用,同时下面的setTimeout在
            // 函数没有运行的时候,是无法再次设定的.
            if (debounce) clearTimeout(timeout);
            // 如果debouce是true 或者 timeout 为空的情况下,设置setTimeout
            if (debounce || !timeout) timeout = setTimeout(throttler, wait);
        };
    };

    // throttle 节流函数
    _.throttle = function(func, wait) {
        return limit(func, wait, false);
    };

    // debouce 多次调用,只执行最后一次.
    _.debounce = function(func, wait) {
        return limit(func, wait, true);
    };

代码上面都加了注释,比较好理解,而且也比较简单.通过代码,我们可以更加进一步的理解debouncethrottle的原理以及实现,主要都是通过标志位来判断是否要清空setTimeout以及是否要生成新的setTimeout

至此,debouncethrottle的原理以及实现基本就介绍完成了.写的不是特别的流畅,大家凑合着看,主要还是用于记录在日常工作中以及在源码阅读中遇到的一些小发现和小灵感.

参考

jQuery throttle / debounce: Sometimes, less is more!


YusenMeng
6 声望1 粉丝