[JavaScript] 手写实现一个节流函数(Throttle)

1. 什么是节流

​ 节流就是对于连续多次触发事件,事件只在规定时间间隔到了才执行

​ 可以想象成一个沙漏,顶部有很多沙子,但是流下来的沙子却只有一点点,起到了一个限制的作用,不至于全部沙子一哄而下。

​ 在实际应用中,可以用在:

  • 鼠标点击事件:鼠标不断点击,但回调函数只会在规定的时间到时才会运行
  • 监听滚动事件:例如滑到底部自动加载更多,就可以用到节流限制不是每一次滚动都执行,而是滚动时每隔一段时间去执行
  • input输入:input输入进行搜索请求时,也可以使用到节流,每隔一段时间再去发送请求
  • ……

节流与防抖的区别

​ 在[JavaScript] 手写实现一个防抖函数(Debounce)一文中介绍了防抖,读者可能会觉得节流与防抖有点像,其实仔细斟酌就能发现他们的不同

​ 节流是指对于连续触发的事件,每隔一段固定时间执行一次,只要事件持续出发就可以执行很多次。(在节流里涉及的时间主要是指事件执行的间隔时间)

​ 防抖则是对连续触发的事件,只会执行一次,不管事件触发多少次,都只执行一次。(在防抖里设置的时间可以说是对连续触发时间的定义,在设置时间内运行的事件就被称为连续触发的事件

2. 节流的实现(时间戳版)

​ 节流的实现可以利用时间戳,使用这一方法,会立即执行

const throttle = (func, wait) => {
    // 初始化事件开始的时间为0
    let preTime = 0;
    return function() {
        // 下面两行不懂的可以看看防抖实现的那篇文章
        let context = this;
        let args = arguments;
        // 获取当前的时间,使用+来转化为数字类型,方便后面做减法
        let now = +new Date();
        // 当前时间减去之前的时间,结果大于设定的间隔时间才会执行函数
        if (now - preTime > wait) {
            func.apply(context, args);
            preTime = now;
        }
    }
}

3.节流的实现(定时器版)

​ 节流还可以使用定时器实现,使用这一方法,不会立即执行,但会在最后一次停止触发后再执行一次

const throttle2 = (func, wait) => {
    let timeout;
    return function() {
        let context = this;
        let args = arguments;
        // 若没有定时器,说明上一次设定的定时器已到时销毁
        if (!timeout) {
            timeout = setTimeout(function() {
                func.apply(context, args);
                timeout = null;
            }, wait)
        }
    }
}

4. 节流的实现(组合版)

​ 通过前两种方法,可以发现他们在触发的时机上有所区别,我们可不可以将他们结合起来,当然可以!

function throttle3(func, wait){
    let context, args, timeout;
    let pretime = 0;
    let later = function(){
        pretime = +new Date();
        timeout = null;
        func.apply(context, args);
    };
    let throttled = function(){
        context = this;
        args = arguments;
        var now = +new Date();
        var remaining = wait - (now - pretime);
        // 剩余时间为负数表示下一次执行需要立即执行
        // remaining > wait在修改了系统时间的情况下可能发生
        if(remaining <= 0 || remaining > wait){
            // 如果有设置过定时器,清空并置为null
            if(timeout){
                clearTimeout(timeout)
                timeout = null;
            }
            pretime = now;
            func.apply(context,args);
        }else if(!timeout){
            // 需要在剩余时间后执行
            timeout = setTimeout(later,remaining);
        }
    };
    return throttled;
}

5. 节流的实现(自定义版)

​ 在组合版中,实现了可以立即执行,停止触发后再执行一次的效果,但是有时候我们想手动得控制是要立即执行还是停止触发后再执行一次,或者两种效果都要,或者两种效果都不要,让我们来实现一下吧

​ 在组合版的基础上,设置一个options对象参数,根据传的值判断要哪一种效果,规定:

leading:false 表示禁用第一次执行

trailing: false 表示禁用停止触发的回调

function throttle4(func, wait, options) {
    let timeout, context, args, result;
    let pretime = 0;
    // options参数是可选的
    if (!options) options = {};
    
    let later = function() {
        pretime = options.leading === false ? 0 : +new Date();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    }
    
    let throttled = function() {
        context = this;
        args = arguments;
        let now = +new Date();
        // 如果禁用第一次执行,那么将上一次执行的时间于当前时间相等即可
        if (!pretime && options.leading === false) pretime = now;
        let remaining = wait - (now - pretime);
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            pretime = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
            // 允许停止触发后执行回调函数,只有当traling为true时才会执行下面的代码
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };
    return throttled
}

// 测试
container.onmousemove = throttle4(getUserAction,2000, {leading: false, trailing: true});

让我来详细解释一下

  1. 使用时间戳的方法,是可以达到第一次触发马上执行的效果,使用定时器的方法,可以达到停止触发后再执行一次的效果。
  2. 当设置了leading:truetrailing: false 允许第一次执行时,且不允许停止触发后再执行一次,当前时间pretime的初始值为0,接下来就一直使用时间戳的方法进行节流。
  3. 当设置了leading:truetrailing: true允许第一次执行,且允许停止触发后再执行一次,那么会在间隔时间大于等于wait的情况下,使用时间戳进行节流(达到第一次执行的效果),在间隔时间小于wait去触发时,设置定时器进行节流 (达到停止触发后再执行一次的效果),这就和我们的组合版相同。
  4. 当设置了leading:falsetrailing: true禁止第一次执行,允许停止触发后再执行一次,当前时间的初始值为now(达到禁止第一次执行的效果),在使用定时器方法时,将pretime设置为0(pretime为0是判断是否为第一次执行的条件,如果同时满足pretime===0,leading===false,就会将pretime赋值为当前时间now)。
  5. 当设置了leading:falsetrailing: false禁止第一次执行,禁止停止触发后再执行一次,当前时间pretime的初始值为now(达到禁止第一次执行的效果)同时不会使用定时器的方式。

6. 小结

​ 本篇文章中,实现了节流的四个版本:时间戳实现,定时器实现,组合实现,自定义实现。我们最后实现的版本与underscore中的节流是一样的,在underscore中还添加了一个取消节流的函数,实现方法也很简单,有兴趣的同学可以去这个链接了解一下underscore/throttle.js

​ 今天的文章就到这里啦,我们下次再见~

posted @ 2021-09-11 16:43  是棕啊  阅读(1317)  评论(0编辑  收藏  举报