JS专题之节流函数

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

一、什么是节流?

节流函数(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 都是为了尽可能满足这类工具函数的不同使用场景。

欢迎关注我的个人公众号“谢南波”,专注分享原创文章。

掘金专栏 JavaScript 系列文章

  1. JavaScript之变量及作用域
  2. JavaScript之声明提升
  3. JavaScript之执行上下文
  4. JavaScript之变量对象
  5. JavaScript之原型与原型链
  6. JavaScript之作用域链
  7. JavaScript之闭包
  8. JavaScript之this
  9. JavaScript之arguments
  10. JavaScript之按值传递
  11. JavaScript之例题中彻底理解this
  12. JavaScript专题之模拟实现call和apply
  13. JavaScript专题之模拟实现bind
  14. JavaScript专题之模拟实现new
  15. JS专题之事件模型
  16. JS专题之事件循环
  17. JS专题之去抖函数

猜你喜欢

转载自juejin.im/post/5c114b54518825275318ba1a
今日推荐