前端性能优化之节流-throttle

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

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

专业术语归纳就是:每隔必定时间,执行一次函数。java

最简易版的代码实现:性能优化

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的时机则是定时器执行完毕。闭包

除了定时器,还能够用时间戳实现:app

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:post

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,下一次再次触发又会当即执行,这样就有冲突了。

相关文章
相关标签/搜索