zoukankan      html  css  js  c++  java
  • 简述JavaScript函数节流

    为什么要用函数节流

    浏览器中某些计算和处理要比其他的昂贵很多。例如,DOM 操作比起非 DOM 交互需要更多的内存和 CPU 时间。连续尝试进行过多的 DOM 相关操作可能会导致浏览器挂起,有时候甚至会崩溃。尤其在 IE 中使用 onresize 事件处理程序的时候容易发生,当调整浏览器大小的时候,该事件会连续触发。在 onresize 事件处理程序内部如果尝试进行 DOM 操作,其高频率的更改可能会让浏览器崩溃。为了绕开这个问题,你可以使用定时器对该函数进行节流。

    常见的有:重新调整浏览器窗口大小(resize),浏览器页面滚动(scroll),鼠标移动(mousemove)。

    函数节流的基本思想

    函数节流背后的基本思想是指,某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数,创建一个定时器,在指定的时间间隔之后运行代码。当第二次调用该函数时,它会清除前一次的定时器并设置另一个。如果前一个定时器已经执行过了,这个操作就没有任何意义。然而,如果前一个定时器尚未执行,其实就是将其替换为一个新的定时器。目的是只有在执行函数的请求停止了一段时间之后才执行。

    说白一点就是:当你改变窗体大小的时候,不管你怎么拉,怎么拽,都没有立刻生效,而是在你改变完大小后的一会儿,它的内容才进行缩放适应。

    该模式的基本形式:

    var processor = {
      timeoutId: null,
      // 实际进行处理的方法
      performProcessing: function(){
        // 实际执行的代码
      },
      // 初始处理调用的方法
      process: function(){
        clearTimeout(this.timeoutId);
        var that = this;
        this.timeoutId = setTimeout(function(){
          that.performProcessing();
        }, 100);
      }
    };
    // 尝试开始执行
    
    processor.process();

    在这段代码中,创建了一个叫做 processor 的对象。这个对象还有2个方法:process() 和 performProcessing()。前者是初始化任何处理所必须调用的,后者则实际进行应完成的处理。当调用了 process(),第一步是清除存好的 timeoutId,来阻止之前的调用被执行。然后,创建一个新的定时器调用 performProcessing()。由于 setTimeout() 中用到的函数的环境总是 window ,所以有必要保存 this 的引用以方便以后使用。

    时间间隔设为了 100ms,这表示最后一次调用 process() 之后至少 100ms 后才会调用 performProcessing()。所以如果 100ms 之内调用了 process() 共20次,performProcessing() 仍只会被调用一次。

    这个模式可以使用 throttle() 函数来简化

    这个函数可以自动进行定时器的设置和清除,如下例所示:

    function throttle(method, context){
      clearTimeout(method.tId);
      method.tId = setTimeout(function(){
        method.call(context);
      }, 100);
    }    

    throttle() 函数接受两个参数:要执行的函数以及在哪个作用域中执行。上面这个函数首先清除之前设置的任何定时器。定时器 ID 是存储在函数的 tId 属性中的,第一次把方法传递给 throttle() 的时候,这个属性可能并不存在。接下来,创建一个新的定时器,并将其 ID 存储在方法的 tId 属性中。如果这是第一次对这个方法调用 throttle() 的话,那么这段代码会创建该属性。定时器代码使用 call() 来确保方法在适当的环境中执行。如果没有给出第二个参数,那么就在全局作用域内执行该方法。

    前面提到过,节流在 resize 事件中是最常用的。如果你基于该事件来改变页面布局的话,最好控制处理的频率,以确保浏览器不会在极短的时间内进行过多的计算。例如,假设有一个 <div/> 元素需要保持它的高度始终等于宽度。那么实现这一功能的 JavaScript 可以如下编写:

    window.onresize = function(){
      var div = document.getElementById("myDiv");
      div.style.height = div.offsetWidth + "px";
    };

    这段非常简单的例子有两个问题可能会造成浏览器运行缓慢。首先,要计算 offsetWidth 属性,如果该元素或者页面上其他元素有非常复杂的 CSS 样式,那么这个过程将会很复杂。其次,设置某个元素的高度需要对页面进行回流来令改动生效。如果页面有很多元素同时应用了相当数量的 CSS 的话,这又需要很多计算。这就可以用到 throttle() 函数,如下例所示:

    function resizeDiv(){
      var div = document.getElementById("myDiv");
      div.style.height = div.offsetWidth + "px";
    }
    
    window.onresize = function(){
      throttle(resizeDiv);
    };

    这里,调整大小的功能被放入了一个叫做 resizeDiv() 的单独函数中。然后 onresize 事件处理程序调用 throttle() 并传入 resizeDiv 函数,而不是直接调用 resizeDiv() 。多数情况下,用户是感觉不到变化的,虽然给浏览器节省的计算可能会非常大。

    只要代码是周期性执行的,都应该使用节流,但是你不能控制请求执行的速率。这里展示的 throttle() 函数用了 100ms 作为间隔,你当然可以根据你的需要来修改它。

    再来看一个封装的方法:

    var throttle = function(fn, delay){
         var timer = null;
         return function(){
             var context = this, args = arguments;
             clearTimeout(timer);
             timer = setTimeout(function(){
                 fn.apply(context, args);
             }, delay);
         };
     };

    它使用闭包的方法形成一个私有的作用域来存放定时器变量timer。而调用方法为

    window.onresize = throttle(myFunc, 100);

    两种方法各有优劣,前一个封装函数的优势在把上下文变量当做函数参数,直接可以定制执行函数的this变量;后一个函数优势在于把延迟时间当做变量(当然,前一个函数很容易做这个拓展)。

    使用闭包代码结构会更优,且易于拓展定制其他私有变量,缺点就是虽然使用apply把调用throttle时的this上下文传给执行函数,但毕竟不够灵活。

    这里主要了解一点:throttle 被调用后返回的function才是真正的onresize 触发时需要调用的函数

    现在看起来这个方法已经接近完美了,然而实际使用中并非如此。举个例子:
    如果用户不断的resize浏览器窗口大小,这时延迟处理函数是一次都不会执行的

    于是我们又要添加一个功能:当用户触发resize的时候应该在某段时间内至少触发一次,既然是在某段时间内,那么这个判断条件就可以取当前的时间毫秒数,每次函数调用把当前的时间和上一次调用时间相减,然后判断差值如果大于某段时间就直接触发,否则还是走 timeout 的延迟逻辑。

    var throttleV2 = function(fn, delay, mustRunDelay){
         var timer = null;
         var t_start;
         return function(){
             var context = this, args = arguments, t_curr = +new Date();
             clearTimeout(timer);
             if(!t_start){
                 t_start = t_curr;
             }
             if(t_curr - t_start >= mustRunDelay){
                 fn.apply(context, args);
                 t_start = t_curr;
             }
             else {
                 timer = setTimeout(function(){
                     fn.apply(context, args);
                 }, delay);
             }
         };
     };

    在这个拓展后的节流函数升级版,我们可以设置第三个参数,即必然触发执行的时间间隔。如果用下面的方法调用

    window.onresize = throttleV2(myFunc, 50, 100);

    则意味着,50ms的间隔内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔100ms至少执行一次(即在100ms内,如果还是在触发的话,还是会执行一次)。原理也很简单,打时间tag,一开始记录第一次调用的时间戳,然后每次调用函数都去拿最新的时间跟记录时间比,超出给定的时间就执行一次,更新记录时间。

    示例:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title>throttle</title>
    </head>
    <body>
    <div style="height:5000px">
    <div id="demo" style="position:fixed;"></div>
    </div>
    <script>
    var COUNT = 0, demo = document.getElementById('demo');
    function testFn() {demo.innerHTML += 'testFN 被调用了 ' + ++COUNT + '次<br>';}
    
    var throttle = function(fn, delay, mustRunDelay){
         var timer = null;
         var t_start;
         return function(){
             var context = this, args = arguments, t_curr = +new Date();
             clearTimeout(timer);
             if(!t_start){
                 t_start = t_curr;
             }
             if(t_curr - t_start >= mustRunDelay){
                 fn.apply(context, args);
                 t_start = t_curr;
             }
             else {
                 timer = setTimeout(function(){
                     fn.apply(context, args);
                 }, delay);
             }
         };
     };
    // window.onscroll = throttle(testFn, 200);
    window.onscroll = throttle(testFn, 500, 1000);
    </script>
    </body>
    </html>

    参考:

    《JavaScript高级程序设计》

    浅谈javascript的函数节流 

    JavaScript函数节流

    JavaScript节流函数throttle详解

  • 相关阅读:
    小波变换的引入,通俗易懂
    Leetcode 437. Path Sum III
    Leetcode 113. Path Sum II
    Leetcode 112 Path Sum
    Leetcode 520 Detect Capital
    Leetcode 443 String Compression
    Leetcode 38 Count and Say
    python中的生成器(generator)总结
    python的random模块及加权随机算法的python实现
    leetcode 24. Swap Nodes in Pairs(链表)
  • 原文地址:https://www.cnblogs.com/Chen-XiaoJun/p/6217002.html
Copyright © 2011-2022 走看看