美文网首页让前端飞
前端性能优化之节流-throttle

前端性能优化之节流-throttle

作者: xshinei | 来源:发表于2018-11-19 15:07 被阅读13次

    上次介绍了前端性能优化之防抖-debounce,这次来聊聊它的兄弟-节流。

    再拿乘电梯的例子来说:坐过电梯的都知道,在电梯关门但未上升或下降的一小段时间内,如果有人从外面按开门按钮,电梯是会再开门的。要是电梯空间没有限制的话,那里面的人就一直在等。。。后来电梯工程师收到了好多投诉,于是他们就改变了方案,设定每隔一定时间,比如30秒,电梯就会关门,下一节电梯会继续等待30秒。

    专业术语概括就是:每隔一定时间,执行一次函数。

    最简易版的代码实现:

    function throttle(fn, delay) {
        let timer = null;
    
        return function() {
            const context = this;
            const args = arguments;
    
            if (!timer) {
                timer = setTimeout(() => {
                    fn.apply(context, args);
                    timer = null;
                }, delay);
            }
        };
    }
    

    很好理解,返回一个匿名函数形成闭包,并维护了一个局部变量timer。只有在timer不为null才开启定时器,而timer为null的时机则是定时器执行完毕。

    除了定时器,还可以用时间戳实现:

    function throttle(fn, delay) {
        let last = 0;
    
        return function() {
            const context = this;
            const args = arguments;
    
            const now = +new Date();
            const offset = now - last;
    
            if (offset > delay) {
                last = now;
                fn.apply(context, args);
            }
        };
    }
    

    last代表上次执行fn的时刻,每次执行匿名函数都会计算当前时刻与last的间隔,是否比我们设定的时间间隔大,若大于,则执行fn,并更新last的值。

    比较上述两种实现方式,其实是有区别的:
    定时器方式,第一次触发并不会执行fn,但停止触发之后,还会再次执行一次fn
    时间戳方式,第一次触发会执行fn,停止触发后,不会再次执行一次fn

    两种方式是可以互补的,可以将其结合起来,即能第一次触发会执行fn,又能在停止触发后,再次执行一次fn:

    function throttle(fn, delay) {
        let last = 0;
        let timer = null;
    
        return function() {
            const context = this;
            const args = arguments;
    
            const now = +new Date();
            const offset = now - last;
    
            if (offset > delay) {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
    
                last = now;
                fn.apply(context, args);
            }
            else if (!timer) {
                timer = setTimeout(() => {
                    last = +new Date();
                    timer = null;
                    fn.apply(context, args);
                }, delay - offset);
            }
        };
    }
    

    匿名函数内有个if...else,第一个是判断时间戳,第二个是判断定时器,对比下前面两种实现方式。
    首先是时间戳方式的简易版:

    if (offset > delay) {
      last = now;
      fn.apply(context, args);
    }
    

    混合版:

    if (offset > delay) {
      if (timer) {      // 注意这里
        clearTimeout(timer);
        timer = null;
      }
    
      last = now;
      fn.apply(context, args);
    }
    

    可以发现,混合版比简易版多了对timer不为null的判断,并清除了定时器、将timer置为null。
    再是定时器实现方式的简易版:

    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null;
      }, delay);
    }
    

    混合版:

    else if (!timer) {
      timer = setTimeout(() => {
        last = +new Date();   // 注意这里
        timer = null;
        fn.apply(context, args);
      }, delay - offset);
    }
    

    可以看到,混合版比简易版多了对last变量的重置,而last变量是时间戳实现方式中判断的重要因素。这里要注意下,因为是在定时器的回调中,所以last的重置值要重新获取当前时间戳,而不能使用变量now。

    通过以上对比,我们可以发现,混合版是综合了两种不同实现方式的作用,但除去开始和结束阶段的不同,两者的共同作用是一致的--执行fn函数。所以,同一个时刻,执行fn函数的语句只能存在一个!在混合版的实现中,时间戳判断里,去除了定时器的影响,定时器判断里,去除了时间戳的影响。

    对于立即执行和停止触发后的再次执行,我们可以通过参数来控制,适应需求的变化。
    假设规定{ immediate: false } 阻止立即执行,{ trailing: false } 阻止停止触发后的再次触发:

    function throttle(fn, delay, options = {}) {
        let timer = null;
        let last = 0;
    
        return function() {
            const context = this;
            const args = arguments;
    
            const now = +new Date();
            
            if (last === 0 && options.immediate === false) {    // 这个条件语句是新增的
                last = now;
            }
    
            const offset = now - last;
    
            if (offset > delay) {
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
    
                last = now;
                fn.apply(context, args);
            }
            else if (!timer && options.trailing !== false) {  // options.trailing !== false 是新增的
                timer = setTimeout(() => {
                    last = options.immediate === false ? 0 : +new Date();;
                    timer = null;
                    fn.apply(context, args);
                }, delay - offset);
            }
        };
    }
    

    相对于混合版,除了新增了一个参数options,其它不同之处已在代码中标明。
    思考下,立即执行是时间戳方式实现的,那么想要阻止立即执行的话,只要阻止第一次触发时,offset > delay 条件的成立就行了!如何判断是第一次触发?last变量只有初始化时,值才会是0,再加上我们手动传入的参数,阻止立即执行的条件就满足了:

    if (last === 0 && options.immediate === false) {    
      last = now;
    }
    

    条件满足后,我们重置last变量的初始值为当前时间戳,那么第一次 offset > delay 就不会成立了!
    然后想阻止停止触发后的再次执行,仔细一想,要是不需要这个功能的话,时间戳的实现不就可以满足了?对!我们只要变相地去除定时器就好了:

    !timer && options.trailing !== false
    

    如果我们不手动传入{ trailing: false } ,这个条件是永远不会成立的,即定时器永远不会开启。

    不过有个问题在于,immediate和trailing不能同时设置为false,原因在于,{ trailing: false } 的话,停止触发后不会再次执行,然后关键的last变量也就不会被重置为0,下一次再次触发又会立即执行,这样就有冲突了。

    相关文章

      网友评论

        本文标题:前端性能优化之节流-throttle

        本文链接:https://www.haomeiwen.com/subject/hssgfqtx.html