你不像块宝却像根草 發表於 2019-8-22 16:34:00

[JavaScript] 节流(throttle)-防抖(debounce) 不懵圈指北

<hr>
<h1 id="1认识throttle节流与debounce防抖">1.认识<code>throttle</code>(节流)与<code>debounce</code>(防抖)</h1>
<blockquote>
<p><code>throttle</code>(节流)与<code>debounce</code>(防抖)</p>
</blockquote>
<p><code>throttle</code>和<code>debounce</code>是解决请求和响应速度不匹配问题的两个方案。</p>
<p>二者的差异在于选择不同的策略。</p>
<p><code>debounce</code>的关注点是空闲的间隔时间,</p>
<p><code>throttle</code>的关注点是连续的执行间隔时间。</p>
<blockquote>
<p>应用场景</p>
</blockquote>
<p>只要涉及到连续事件或频率控制相关的应用就可以考虑使用这两个函数,比如:</p>
<ul>
<li>游戏设计,<code>keydown</code>事件</li>
<li>文本输入、自动完成,<code>keyup</code>事件</li>
<li>鼠标移动<code>mousemove</code>事件</li>
<li><code>DOM</code>元素动态定位,<code>window</code>对象的<code>resize</code>、<code>scroll</code>事件</li>
</ul>
<p>前两者,<code>debounce</code>和<code>throttle</code>都可以按需使用;<br>
后两者就要用<code>throttle</code>了</p>
<p>(以上摘自网易云课堂微专业前端高级课程)</p>
<blockquote>
<p>上手试试</p>
</blockquote>
<p>很容易想到的应用场景就是,用户输入,<br>
<code>wait</code>间隔毫秒数,就是用户操作停顿的事件,</p>
<p>如果间隔非常小,就视为没有停顿;<br>
如果超过设定的值,就认为停顿了,发起<code>web</code>请求。</p>
<p>但我们这里用页面滚动<code>scroll</code>事件来举例(观测简便)。</p>
<h2 id="没有防抖与节流的操作">没有防抖与节流的操作</h2>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt;
    &lt;title&gt;debounce sample&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div style="height: 1500px;"&gt;&lt;/div&gt;

    &lt;script&gt;
      // scroll in normal
      window.addEventListener('scroll', function(){
            console.log('normal scrolling...')
      })
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>打开样例页面的控制台并滚动鼠标滚动轮,会发现随着滚动不停地打印出<code>log</code>信息。</p>
<h2 id="debounce">debounce</h2>
<ul>
<li>
<p>原理</p>
<p>对于设定的间隔时间(通常为毫秒)内的交互,只响应最新的,之前的全部忽略掉。</p>
<p>用一个形象的例子来说明:</p>
<blockquote>
<p>用户(比如你)跟baidu说,我想搜索点不可告人的学习资料,<br>
baidu:你确定吗?1500毫秒内不回复的话,我就检索了<br>
--未超过1500毫秒,重新/继续 输入--<br>
用户(比如你):更改搜索内容为“JS中防抖节流的原理与应用”<br>
baidu:你确定吗?1500毫秒内不回复的话,我就检索了<br>
--超过1500毫秒未输入--<br>
baidu:已发起检索请求,这是结果(JS中防抖节流的原理与应用)<br>
--此时再输入--<br>
用户(比如你):我还是想看点令人愉悦的龌龊内容<br>
--回到对话开始时的状态,以下重复省略--<br>
1500毫秒只是打个比方,实际比这个值要小</p>
</blockquote>
<p>(其实在搜索中,防抖与节流都有使用,后面会提到)</p>
</li>
<li>
<p>手写一下简单的<code>debounce</code>函数实现</p>
<pre><code class="language-js">// 简单的`debounce`函数实现
var debounce = function(func, wait){
    var timer   // 定时器

    // 返回包装过的debounce函数
    return function(...args){
      // 如果有触发,则取消之前的触发,以当前触发为准,重新计时
      if(timer){
            clearTimeout(timer)
      }

      // 设置定时器
      timer = setTimeout(function(){
            // 定时器的回调函数:清除本次定时器,并执行函数
            clearTimeout(timer)
            timer = null
            func.apply(null, args)
      }, wait)
    }
}
</code></pre>
</li>
</ul>
<blockquote>
<p>使用<code>debounce</code>的例子</p>
</blockquote>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt;
    &lt;title&gt;debounce sample&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
       &lt;div style="height: 1500px;"&gt;&lt;/div&gt;

       &lt;script&gt;
         // debounce-definition
         // scroll in debounce
         var debounceFnc = debounce(function(){
               console.log('scroll in debounce')
         }, 1500)
         window.addEventListener('scroll', debounceFnc)
       &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>打开上面例子页面的控制台,可以看到,当滚动鼠标滚动轮(或拖动滚动条),</p>
<p>1500毫秒内,不管滚动多少次,都会在停止并经过1500毫秒后,只执行一次。</p>
<h2 id="throttle">throttle</h2>
<ul>
<li>
<p>原理</p>
<p>达到设定的时间间隔才可以触发,控制调用的频率。</p>
<p>用一个形象的例子来说明,</p>
<blockquote>
<p>就像是游戏中放大后的冷却,放一个大之后,<br>
大招就不可用了,需要等待冷却时间过后才可以再发一次。</p>
</blockquote>
</li>
<li>
<p>手写一个简单的<code>throttle</code>函数实现</p>
<pre><code class="language-js">// 简单的`throttle`函数实现
var throttle = function(func, wait){
    var lastTime = 0    // 用来记录上次执行的时刻

    // 返回包装过的throttle函数
    return function(...args){
      var now = Date.now()
      
      var coolingDown = now - lastTime &lt; wait
      // ↑ 距离上次执行的间隔,小于设定的间隔时间 =&gt; 则处于冷却时间

      // 冷却时间,禁止放大招
      if(coolingDown){
            return
      }

      // 记录本次执行的时刻
      lastTime = Date.now()
      
      // 冷却好了就要放大招
      func.apply(null, args)
    }
}
</code></pre>
</li>
</ul>
<blockquote>
<p>使用<code>throttle</code>的例子</p>
</blockquote>
<pre><code class="language-html">&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
    &lt;meta charset="UTF-8"&gt;
    &lt;meta name="viewport" content="width=device-width, initial-scale=1.0"&gt;
    &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt;
    &lt;title&gt;throttle sample&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
    &lt;div style="height: 1500px;"&gt;&lt;/div&gt;

    &lt;script&gt;
      // throttle-definition
      // scroll in throttle
      var throttleFnc = throttle(function(){
            console.log('scroll in throttle')
      }, 1500)
      window.addEventListener('scroll', throttle)
    &lt;/script&gt;
&lt;/body&gt;
&lt;/html&gt;
</code></pre>
<p>打开上面例子页面的控制台,可以看到,当滚动鼠标滚动轮(或拖动滚动条),</p>
<p>不管滚动地多快甚至一直滚动不停,每次执行(打印<code>log</code>信息)都会间隔1500毫秒。</p>
<blockquote>
<p>小小总结</p>
</blockquote>
<p>对比上面的<code>debounce</code>防抖与<code>throttle</code>节流,</p>
<ul>
<li>相同
<ul>
<li><code>debounce</code>防抖与<code>throttle</code>节流都实现了单位时间内,函数只执行一次</li>
</ul>
</li>
<li>不同
<ul>
<li><code>debounce</code>防抖:<br>
单位时间内,忽略前面的,响应最新的,并在延迟<code>wait</code>毫秒后执行</li>
<li><code>throttle</code>节流:<br>
响应第一次的,单位时间内,不再响应,直到<code>wait</code>毫秒后才再响应</li>
</ul>
</li>
</ul>
<blockquote>
<p>咱之前没做过前端(实际上是就没怎么接触过<code>web</code>),<br>
就没听说过<code>underscore</code>这个工具库,直到在<code>网易云课堂·微专业</code><br>
学习<code>前端高级开发工程师</code>的课程,才认识到了<code>underscore</code>。<br>
(广告过后,)咱们来看看<code>underscore</code>中可配置的<code>throttle</code>节流与<code>debounce</code>防抖。</p>
</blockquote>
<hr>
<h1 id="2javascript工具箱underscore中的throttle节流与debounce防抖">2.<code>JavaScript</code>工具箱<code>underscore</code>中的<code>throttle</code>(节流)与<code>debounce</code>(防抖)</h1>
<p>看了上面简单实现的代码样例,让我们再来了解下<code>underscore</code>中的节流与防抖</p>
<blockquote>
<p>引入<code>underscore.js</code>文件或<code>cdn</code>地址:<br>
<code>&lt;script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js"&gt;&lt;/script&gt;</code></p>
</blockquote>
<h2 id="1-throttle-节流">1) throttle 节流</h2>
<pre><code class="language-js">_.throttle(function, wait, )
</code></pre>
<table>
<thead>
<tr>
<th>参数列表</th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>function</td>
<td>处理函数</td>
</tr>
<tr>
<td>wait</td>
<td>指定的毫秒数间隔</td>
</tr>
<tr>
<td>options</td>
<td>配置</td>
</tr>
<tr>
<td></td>
<td>可选。默认:<code>{ leading: false, trailing: false }</code></td>
</tr>
</tbody>
</table>
<p>针对第一次触发,</p>
<p>leading : true 相当于先执行,再等待<code>wait</code>毫秒之后才可再次触发<br>
trailing : true 相当于先等待<code>wait</code>毫秒,后执行</p>
<p>默认:<br>
leading : false =&gt; 阻止第一次触发时立即执行,等待<code>wait</code>毫秒才可触发<br>
trailing : false =&gt; 阻止第一次触发时的延迟执行,经过延迟的<code>wait</code>毫秒之后才可触发</p>
<p>可能的配置方式:<br>
(区别在首次执行和先执行还是先等待)</p>
<table>
<thead>
<tr>
<th>配置</th>
<th>结果</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>{ leading: false, trailing: false }</code></td>
<td>第一次触发不执行,后面同普通<code>throttle</code>,执行 + 间隔<code>wait</code>毫秒</td>
</tr>
<tr>
<td><code>{ leading: true, trailing: false }</code></td>
<td>第一次触发立即执行,后面同普通<code>throttle</code>,执行 + 间隔<code>wait</code>毫秒</td>
</tr>
<tr>
<td><code>{ leading: false, trailing: true }</code></td>
<td>每次触发延迟执行,每次执行间隔<code>wait</code>毫秒</td>
</tr>
<tr>
<td><code>{ leading: true, trailing: true }</code></td>
<td>每一次有效触发都会执行两次,先立即执行一次,后延时<code>wait</code>毫秒执行一次</td>
</tr>
</tbody>
</table>
<p>知道了原理,我们来简单写一下代码实现。</p>
<pre><code class="language-js">_.now = Date.now

_.throttle = function(func, wait, options){
    var lastTime = 0
    var timeOut = null
    var result
    if(!options){
      options = { leading: false, trailing: false }
    }

    return function(...args){// 节流函数
      var now = _.now()

      // 首次执行看是否配置了 leading = false = 默认,阻止立即执行
      if(!lastTime &amp;&amp; options.leading === false){
            lastTime = now
      }
      // 配置了 leading = true 时,初始值 lastTime = 0,即可以立即执行

      var remaining = lastTime + wait - now
      // &gt; 0 即间隔内
      // &lt; 0 即超出间隔时间

      // 超出间隔时间,或首次的立即执行
      if(remaining &lt;= 0){   // trailing=false
            if(timeOut){
                // 如果不是首次执行的情况,需要清空定时器
                clearTimeout(timeOut)
                timeOut = null
            }
            lastTime = now      // #
            result = func.apply(null, args)
      }
      else if(!timeOut &amp;&amp; options.trailing !== false){    // leading
            // 没超出间隔时间,但配置了 leading=fasle 阻止了立即执行,
            // 即需要执行一次却还未执行,等待中,且配置了 trailing=true
            // 那就要在剩余等待毫秒时间后触发
            timeOut = setTimeout(()=&gt;{
                lastTime = options.leading === false ? 0 : _.now()      // # !lastTime 的判断中需要此处重置为0
                timeOut = null
                result = func.apply(null, args)
            }, remaining);
      }

      return result
    }
}
</code></pre>
<hr>
<h2 id="2-debounce-防抖">2) debounce 防抖</h2>
<pre><code class="language-js">    _.debounce(func, wait, )
</code></pre>
<table>
<thead>
<tr>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr>
<td>function</td>
<td>处理函数</td>
</tr>
<tr>
<td>wait</td>
<td>指定的毫秒数间隔</td>
</tr>
<tr>
<td>immediate</td>
<td>立即执行Flag</td>
</tr>
<tr>
<td></td>
<td>可选。默认:<code>false</code></td>
</tr>
</tbody>
</table>
<p>功能解析:</p>
<p>前两个参数与前面介绍的<code>throttle</code>是一样的,第三个参数,<br>
<code>immediate</code>指定第一次触发或没有等待中的时候可以立即执行。</p>
<p>知道了原理,我们来简单写一下代码实现。</p>
<pre><code class="language-js">// debounce 防抖:
// 用户停止输入&amp;wait毫秒后,响应,
// 或 immediate = true 时,没有等待的回调的话立即执行
// 立即执行并不影响去设定时器延迟执行
_.debounce = function(func, wait, immediate){
    var timer, result

    var later = function(...args){
      clearTimeout(timer)
      timer = null
      result = func.apply(null, args)
    }

    return function(...args){
      // 因为防抖是响应最新一次操作,所以清空之前的定时器
      if(timer) clearTimeout(timer)

      // 如果配置了 immediate = true
      if(immediate){
            // 没有定时函数等待执行才可以立即执行
            var callNow = !timer

            // 是否立即执行,并不影响设定时器的延迟执行
            timer = setTimeout(later, wait, ...args)

            if(callNow){
                result = func.apply(null, args)
            }
      }
      else{
            timer = setTimeout(later, wait, ...args)
      }

      return result
    }
}
</code></pre>
<p>代码添加了注释作为说明内容,应该很容易理解。</p>
<hr>
<blockquote>
<p>看<code>underscore.js</code>最新源码(2019-08-22当前:1.9.1)的话会发现,<br>
除了上文介绍的配置,还加入了可取消功能(cancel)(from 1.9.0)</p>
</blockquote>
<hr>
<h1 id="3重新审视throttle节流与debounce防抖">3.重新审视<code>throttle</code>(节流)与<code>debounce</code>(防抖)</h1>
<p><code>underscore.js</code>中通过增加可配置项来实现精细控制以应对使用者的不同需求。</p>
<p>其实我们一般的需求,应该是这两种基础功能结合在一起应用。</p>
<p> </p>
<p>再回到最初我们自己写的极简示例<code>demo</code>函数上,</p>
<p>在没有任何配置的基本的实现里,<code>debounce</code>与<code>throttle</code>的区别在于,<br>
当鼠标一直在滚动,<code>debounce</code>会一直等待结束后<code>wait</code>毫秒再执行,<br>
而<code>throttle</code>会每间隔<code>wait</code>毫秒就执行一次。</p>
<p>再比如还是以用户输入为例,极端的例子,一直输入没有停的情况下,</p>
<p>输入了十分钟,结果页面在这十分钟内是没有反应的,</p>
<p>停止输入并经过<code>wait</code>毫秒之后,才会有响应,针对这种极端情况,</p>
<p>就用到了<code>debounce</code>与<code>throttle</code>组合。</p>
<p>一直输入的过程中,按照<code>debounce</code>是不会触发响应的,但超过了节流阀</p>
<p><code>throttle</code>设定的<code>wait</code>,那至少会执行一次。</p>
<p>这样,就完善了用户输入的体验。</p>
<p>其它交互同理,一般将两者结合使用。</p>
<p>Talk is cheep, show you the code.</p>
<pre><code class="language-js">// --------------------------------------------------
function combineDebounceThrottle(func, wait){
    var lastTime = 0
    var timeoutD
    var timeoutT

    var later = function(...args){
      clearTimeout(timeoutD)
      clearTimeout(timeoutT)
      timeoutD = null
      timeoutT = null
      lastTime = Date.now()
      func.apply(null, args)
    }

    return function(...args){
      var now = Date.now()
      var coolingDown = now- lastTime &lt; wait

      clearTimeout(timeoutD)

      if(!timeoutT &amp;&amp; !coolingDown){
            timeoutT = setTimeout(later, wait, 'throttle',...args)
      }
      else{
            timeoutD = setTimeout(later, wait, 'debounce',...args)
      }
    }
}

// --------------------------------------------------

var func = combineDebounceThrottle(function(logFlag){
    console.log('scrolling in throttle-debounce-combined')
}, 1500)

window.addEventListener('scroll', func)

// --------------------------------------------------
</code></pre>
<p>啰嗦两句,<br>
只用 debounce 的话,一直滚动就会一直等待,<br>
加入了 throttle,则超过 wait 毫秒就会执行一次,throttle 延迟执行等待中的时候,<br>
仍然有输入则在结束后,还会触发 debounce 的延时回调。</p>
<hr>
<center>--END--</center>
<hr>


</div>
<div id="MySignature" role="contentinfo">
    <style>.signature { background-color: lightskyblue; padding: 10px; display: inline-flex; border: 1px dashed red }
.emphersize { font-weight: bold; color: darkblue }</style>
    <div class="signature" style="display: flex">
      <div>
            <p>作者:<span class="emphersize">码路工人</span></p>
            <p>公众号:<span class="emphersize">码路工人有力量(Code-Power)</span></p>
            <p>欢迎关注个人微信公众号 Coder-Power</p>
            <p>一起学习提高吧~</p>
      </div>
      <img src="https://gitee.com/Coding-Worker/picture/raw/master/2021-1-5/1609860559027-qrcode_for_gh_e1903e0c25a7_258.jpg">
    </div><br><br>
来源:https://www.cnblogs.com/CoderMonkie/p/throttle-and-debounce-in-js.html
頁: [1]
查看完整版本: [JavaScript] 节流(throttle)-防抖(debounce) 不懵圈指北