1
本文共 2000 字,读完只需 8 分钟

上一篇文章讲了去抖函数,然后这一篇讲同样为了优化性能,降低事件处理频率的节流函数。

一、什么是节流?

节流函数(throttle)就是让事件处理函数(handler)在大于等于执行周期时才能执行,周期之内不执行,即事件一直被触发,那么事件将会按每小段固定时间一次的频率执行。

打个比方:王者荣耀、英雄联盟、植物大战僵尸游戏中,技能的冷却时间,技能的冷却过程中,是无法使用技能的,只能等冷却时间到之后才能执行。

那什么样的场景能用到节流函数呢?
比如:

  1. 页面滚动和改变大小时需要进行业务处理,比如判断是否滑到底部,然后进行懒加载数据。
  2. 按钮被高频率地点击时,比如游戏和抢购网站。

我们通过一个简单的示意来理解:

节流函数可以用时间戳和定时器两种方式进行处理。

二、时间戳方式实现

<div class="container" id="container">
    正在滑动:0
</div>

<script>
window.onload = function() {
    var bodyEl = document.getElementsByTagName("body")[0]
}

var count = 0;
window.onmousemove = throttle(eventHandler, 1000);

function eventHandler(e) {
    var containerEl = document.getElementById("container");
    containerEl.innerHTML = "正在滑动: " + count;
    count++;
}

function throttle(func, delay) {
    var delay = delay || 1000;
    var previousDate = new Date();
    var previous = previousDate.getTime();  // 初始化一个时间,也作为高频率事件判断事件间隔的变量,通过闭包进行保存。
    
    return function(args) {
        var context = this;
        var nowDate = new Date();
        var now = nowDate.getTime();
        if (now - previous >= delay) {  // 如果本次触发和上次触发的时间间隔超过设定的时间
            func.call(context, args);  // 就执行事件处理函数 (eventHandler)
            previous = now;  // 然后将本次的触发时间,作为下次触发事件的参考时间。
        }
    }
}
</script>

看时间戳实现版本的效果:

三、定时器方式实现

<div class="container" id="container">
    正在滑动: 0
</div>

<script>
window.onload = function() {
    var bodyEl = document.getElementsByTagName("body")[0]
}

var count = 0;
window.onmousemove = throttle(eventHandler, 1000);

function eventHandler(e) {
    var containerEl = document.getElementById("container");
    containerEl.innerHTML = "正在滑动: " + count;
    count++;
}


function throttle(func, delay) {
    var delay = delay || 1000;
    var timer = null;
    return function(args) {
        var context = this;
        var nowDate = new Date();
        var now = nowDate.getTime();
        if (!timer) {
            timer = setTimeout(function() {
                func.call(context, args);
                timer = null;
            }, delay)

        }
    }

}
</script>

看看定时器版实现版本的效果:

三、时间戳和定时器的对比分析

对比时间戳和定时器两种方式,效果上的区别主要在于:

事件戳方式会立即执行,定时器会在事件触发后延迟执行,而且事件停止触发后还会再延迟执行一次。

具体选择哪种方式取决于使用场景。underscore 把这两类场景用 leading 和 trailing 进行了表示。

四、兼容两种方式, underscore 源码实现

underscore 的源码中就同时实现了时间戳和定时器实现方式,在调用时可以自由选择要不要在间隔时间开始时(leading)执行,或是间隔时间结束后(trailing)执行。

具体看伪代码和示意图:

<div class="container" id="container">
        正在滑动: 0
    </div>
    <div class="height"></div>
    <script>
    window.onload = function() {
        var bodyEl = document.getElementsByTagName("body")[0]
    }

    var count = 0;

    // 事件处理函数
    function eventHandler(e) {
        var containerEl = document.getElementById("container");
        containerEl.innerHTML = "正在滑动: " + count;
        count++;
    }

    var _throttle = function(func, wait, options) {
        var context, args, result;

        // 定时器变量默认为 null, 是为了如果想要触发了一次后再延迟执行一次。
        var timeout = null;

        // 上一次触发事件回调的时间戳。 默认为 0 是为了方便第一次触发默认立即执行
        var previous = 0;

        // 如果没有传入 options 参数
        // 则将 options 参数置为空对象
        if (!options)
            options = {};

        var later = function() {
            // 如果 options.leading === false
            // 则每次触发回调后将 previous 置为 0, 表示下次事件触发会立即执行事件处理函数
            // 否则置为当前时间戳
            previous = options.leading === false ? 0 : +new Date();

            // 剩余时间跑完,执行事件,并把定时器变量置为空,如果不为空,那么剩余时间内是不会执行事件处理函数的,见 else if 那。
            timeout = null;

            result = func.apply(context, args);

            // 剩余时间结束,并执行完事件后,清理闭包中自由变量的内存垃圾,因为不再需要了。
            if (!timeout)
                context = args = null;
        };


        // 返回的事件回调函数
        return function() {
            // 记录当前时间戳
            var now = +new Date();

            // 第一次执行回调(此时 previous 为 0,之后 previous 值为上一次时间戳)
            // 并且如果程序设定第一个回调不是立即执行的(options.leading === false)
            // 则将 previous 值(表示上次执行的时间戳)设为 now 的时间戳(第一次触发时)
            // 表示刚执行过,这次就不用执行了
            if (!previous && options.leading === false)
                previous = now;

            // 间隔时间 和 上一次到本次事件触发回调的持续时间的时间差
            var remaining = wait - (now - previous);

            context = this;
            args = arguments;

            // 如果间隔时间还没跑完,则不会执行任何事件处理函数。
            // 如果超过间隔时间,就可以触发方法(remaining <= 0)

            // remaining > wait,表示客户端系统时间被调整过
            // 也会立即执行 func 函数

            if (remaining <= 0 || remaining > wait) {
                if (timeout) {
                    clearTimeout(timeout);
                    // 解除引用,防止内存泄露
                    timeout = null;
                }

                // 重置前一次触发的时间戳
                previous = now;

                // result 为事件处理函数(handler)的返回值
                // 采用 apply 传递类数组对象 arguments
                result = func.apply(context, args);

                // 引用置为空,防止内存泄露
                if (!timeout)
                    context = args = null;

            } else if (!timeout && options.trailing !== false) {
                // 如果 remaining > 0, 表示在间隔时间内,又触发了一次事件

                // 如果 trailing 为真,则会在间隔时间结束时执行一次事件处理函数(handler)
                // 在从触发到剩余时间跑完,会利用一个定时器执行事件处理函数,并在定时器结束时把 定时器变量置为空

                // 如果剩余事件内已经存在一个定时器,则不会进入本  else if 分支, 表示剩余时间已经有一个定时器在运行,该定时器会在剩余时间跑完后执行。
                // 如果 trailing =  false,即不需要在剩余时间跑完执行事件处理函数。
                // 间隔 remaining milliseconds 后触发 later 方法
                timeout = setTimeout(later, remaining);
            }

            // 回调返回值
            return result;
        };
    };

    window.onmousemove = _throttle(eventHandler, 1000);
    </script>

下面是我画的示意图:

大致总结一下代码对事件处理逻辑的影响:

  1. 如果 leading 为真,那么绿色意味着间隔时间的开始会立即执行,第一次触发也会立即执行。
  2. 如果 trailing 为真,那么从蓝紫色的竖细线后的剩余事件,会跑一个定时器,定时器在时间间隔结束时再执行一次事件处理函数。
  3. 如果 leading 不为真,那么第一次事件触发不会立即执行。
  4. 如果 trailing 不为真,最后一次事件触发后,不然再执行一次事件处理函数。

节流和去抖的常见场景

  1. 输入框打字输入完后才开始异步请求数据校验内容:去抖
  2. 下拉滚动条判断是否到底部,进行懒加载数据:去抖和节流都可以,判断是否到底的方式不同
  3. 活动网站、游戏网站,按钮被疯狂点击:节流

六、总结

去抖和节流函数都是为了降低高频率事件触发的事件处理频率,从而优化网页中大量重绘重排带来的性能问题。

其区别在于去抖会在高频率事件触发时,只执行一次,节流会在满足间隔时间后执行一次。去抖的 immediate,节流中的 leading, trailing 都是为了尽可能满足这类工具函数的不同使用场景。


种瓜南山下
305 声望18 粉丝

南瓜