前端戰(zhàn)五渣學(xué)JavaScript——防抖、節(jié)流和rAF

看了《JavaScript高級程序設(shè)計》和網(wǎng)上的一些博客,感覺對函數(shù)節(jié)流和函數(shù)防抖的概念是反的,以下我寫的關(guān)于防抖和節(jié)流的概念取決于多數(shù)人的概念吧,并且基于倫敦前端工程師David Corbacho的客座文章。文章寫的很好,并且有對應(yīng)的代碼可以操作,更容易理解。其實(shí)我覺得叫什么不重要,這個方法叫節(jié)流還是這個方法叫防抖,只要你能說明白,并且在生產(chǎn)中能用上就可以,一個名字,不用太去糾結(jié)。

《復(fù)仇者聯(lián)盟4:終局之戰(zhàn)》代表著一個時代的結(jié)束,從2008年高二看300多MB的《鋼鐵俠》開始,漫威電影宇宙也像哈利波特的魔法世界一樣一路伴我前行。一個時代的落幕,必將開始一個新的時代。End Game??No!

I LOVE YOU THREE THOUSANDS TIMES

I AM IRON MAN

banner獻(xiàn)給復(fù)仇者聯(lián)盟的超級英雄們

banner獻(xiàn)給復(fù)仇者聯(lián)盟的超級英雄們??????

為什么要防抖和節(jié)流??

防抖節(jié)流是兩個相似的技術(shù),都是為了減少一個函數(shù)無用的觸發(fā)次數(shù),以便提高性能或者說避免資源浪費(fèi)。我們都知道js在操作DOM的時候,代價非常昂貴,相對于非DOM操作需要更多的內(nèi)存和和CPU時間,假如我們一個函數(shù)是在滾動滾動條或者更改更改窗口大小的時候頻繁觸發(fā),還是會出現(xiàn)頁面卡頓,如果是一套復(fù)雜的操作DOM邏輯,可能還會引起瀏覽器崩潰。所以我們需要控制一下觸發(fā)的次數(shù),來優(yōu)化一下代碼執(zhí)行情況。

口說無憑,大家可能也不了解到底是怎樣操作,那就來個例子:??


image
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>我要節(jié)流</title>
  <style>
    body{ height: 3000px; }
    #centerNum { width: 100px; height: 100px; line-height: 100px; text-align: center; position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%); }
  </style>
</head>
<body>
  <h1 id="centerNum">0</h1>
  <script>
    var num = 0;
    window.onscroll = function () {
      var root = document.getElementsByTagName('body'),
      h = document.getElementById('centerNum');
      h.innerHTML = num;
      num ++;
    }
  </script>
</body>
</html>

我們來一個window.onscroll的函數(shù),只要滾動,就改變一次<h1>標(biāo)簽中的數(shù),在上面的圖中,我們能看到這個觸發(fā)是非常頻繁的,如果我們不加以干涉的話,讓這個函數(shù)肆意觸發(fā),豈不是要上天了??

Debounce 防抖

什么是防抖

啥是防抖呢?我自己的理解就是,當(dāng)連續(xù)觸發(fā)一個方法的時候,方法并不執(zhí)行,而是在連續(xù)觸發(fā)結(jié)束的時候再執(zhí)行這個方法。

舉個例子:一部直梯,陸續(xù)往上上人(連續(xù)觸發(fā)),當(dāng)不再上人的時候(停止連續(xù)觸發(fā)),電梯才會關(guān)門并動起來(執(zhí)行方法)。

如何實(shí)現(xiàn)呢

image

上面是我模擬電梯上人的例子做出來的,可能這樣看的比較直觀一些,下面有我實(shí)現(xiàn)的代碼,大概意思就是當(dāng)我上人以后,電梯啟動,當(dāng)我一直在上人的時候,電梯不動直到不再上人了,才會關(guān)門啟動

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>電梯上人</title>
  <style>

  </style>
</head>
<body>
  <button id="addBtn">電梯上人,人數(shù)+1</button><button id="resetBtn">重置</button>
  <p id="personNum">電梯人數(shù):0(假設(shè)電梯可以無限裝人)</p>
  <p id="elevatorStatus">電梯停靠</p>
  <script>
    var personNum = 0; // 電梯人數(shù)
    var closeDoor = null; // 電梯啟動延時程序
    var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
    var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
    var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
    var elevatorStatus = document.getElementById('elevatorStatus'); // 獲取電梯狀態(tài)標(biāo)簽
    /**
     * @method 電梯內(nèi)添加人數(shù)
     * @description 點(diǎn)擊一次電梯內(nèi)增加一人,增加完人數(shù)電梯啟動初始化
     */
    function addPerson() {
      personNum ++;
      personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
      initElevatorStart();
    }
    /**
     * @method 電梯啟動
     * @description 電梯啟動,置灰添加人數(shù)按鈕,禁止上人
     */
    function elevatorStart() {
      elevatorStatus.innerHTML = '電梯啟動';
      addBtn.disabled = true;
    }
    /**
     * @method 電梯啟動初始化
     * @description 清除之前的關(guān)門延時,并重新計算關(guān)門延時500ms,意思是當(dāng)不在觸發(fā)電梯啟動初始化函數(shù)時,500ms后啟動電梯
     */
    function initElevatorStart() {
      clearTimeout(closeDoor);
      closeDoor = setTimeout(function () {
        elevatorStart();
      }, 500);
    }
    /**
     * @method 重置電梯
     */
    function reset() {
      personNum = 0;
      personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
      elevatorStatus.innerHTML = '電梯???;
      addBtn.disabled = false;
    }

    addBtn.addEventListener('click', addPerson);
    resetBtn.addEventListener('click', reset);
  </script>
</body>
</html>

上面的代碼意思就是我電梯上一個人,就需要關(guān)閉電梯門(觸發(fā)initElevatorStart()方法),然后電梯啟動。但是我一直在點(diǎn)擊上人的按鈕,電梯是不會觸發(fā)關(guān)門啟動電梯的elevatorStart()方法。

代碼的核心是initElevatorStart()方法,這個方法在實(shí)際需要執(zhí)行的關(guān)門啟動電梯方法elevatorStart()外面添加了一層setTimeout方法,也就是為了在調(diào)用這個方法的時候我們過500毫秒再去執(zhí)行真正需要執(zhí)行的方法。如果這500毫秒之內(nèi),又重新觸發(fā)了initElevatorStart()方法,就需要重新計時,要不不就夾到人了嘛,要賠錢的。。。。

這是防抖最粗糙的實(shí)現(xiàn)了??????

基本形式

下面是這個防抖實(shí)現(xiàn)的最基本的形式,也是我們在《JavaScript高級程序設(shè)計》中看到的樣子??

var processor = {
  timeoutId: null, // 相當(dāng)于延時setTimeout的一個標(biāo)記,方便清除的時候使用

  // 實(shí)際進(jìn)行處理的方法
  // 連續(xù)觸發(fā)停止以后需要觸發(fā)的代碼
  performProcessiong: function () {
    // 實(shí)際執(zhí)行的代碼
    // 這里實(shí)際就是需要在停止觸發(fā)的時候執(zhí)行的代碼
  },

  // 初始處理調(diào)用的方法
  // 在實(shí)際需要觸發(fā)的代碼外面包一層延時clearTimeout方法,以便控制連續(xù)觸發(fā)帶來的無用調(diào)用
  process: function () {
    clearTimeout(this.timeoutId); // 先清除之前的延時,并在下面重新開始計算時間

    var that = this; // 我們需要保存作用域,因?yàn)橄旅娴膕etTimeout的作用域是在window,調(diào)用不要我們需要執(zhí)行的this.performProcessiong方法
    this.timeoutId = setTimeout(function () { // 100毫秒以后執(zhí)行performProcessiong方法
      that.performProcessiong();
    }, 100) // 如果還沒有執(zhí)行就又被觸發(fā),會根據(jù)上面的clearTimeout來清除并重新開始計算
  }
};

// 嘗試開始執(zhí)行
processor.process(); // 需要重新綁定在一個觸發(fā)條件里

上面這段代碼就是最基本的實(shí)現(xiàn)方式,包在一個對象中,然后在對象中互相調(diào)用,里面的注釋應(yīng)該可以很清楚的說明每一步是干什么呢,最下面的processor.process()我們在實(shí)際使用的時候肯定是需要綁定在一個觸發(fā)條件上的,比如之前的上電梯問題上,我們就需要把processor.process()方法綁定在增加人數(shù)的里面,這樣才會有多次調(diào)用的情況發(fā)生

上面再怎么說都是很簡單的實(shí)現(xiàn),在實(shí)際生產(chǎn)環(huán)境中,邏輯會相對復(fù)雜很多,但是萬變不離其宗,參透了最基礎(chǔ)的,再舉一反三就不是什么問題了

應(yīng)該叫“前搖”??

具體我也不知道應(yīng)該叫啥,英文叫“Leading edge”,甭管中文叫啥了,知道是什么意思就行了。之前我們寫的代碼很明顯可以看出來,在我們連續(xù)觸發(fā)一個方法的時候,是在setTimeout結(jié)束后才去真正執(zhí)行,但是還有一種情況,那就是我們在連續(xù)觸發(fā)一個方法的時候,第一次觸發(fā)就執(zhí)行了,然后后面的連續(xù)觸發(fā)不再執(zhí)行,等連續(xù)觸發(fā)停止,經(jīng)過延時以后,再次觸發(fā)才會真正執(zhí)行。

我還是盜圖吧。。。普遍的形式是下面這種

image

連續(xù)觸發(fā)結(jié)束時執(zhí)行,而我們現(xiàn)在說的“前搖”則是下面這種情況

image

在連續(xù)觸發(fā)的一開始就執(zhí)行了,然后往后的連續(xù)觸發(fā)不執(zhí)行,連續(xù)觸發(fā)停止后再經(jīng)過延時時間后觸發(fā)才會再次執(zhí)行

下面是我自己寫的,大概意思是這樣,代碼實(shí)現(xiàn)也貼出來

image
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>電梯上人</title>
  <style>

  </style>
</head>
<body>
  <button id="addBtn">電梯上人,人數(shù)+1</button><button id="resetBtn">重置</button>
  <p id="personNum">電梯人數(shù):0(假設(shè)電梯可以無限裝人)</p>
  <script>
    var personNum = 0; // 電梯人數(shù)
    var okNext = true; // 是否可進(jìn)行下次執(zhí)行
    var timeoutFn = null;
    var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
    var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
    var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
    /**
     * @method 電梯添加人數(shù)
     * @description 電梯可以上人,但是上人以后就不能再上了,不管怎么觸發(fā)都不行,除非停止觸發(fā)500毫秒以后,再觸發(fā)的時候才可以繼續(xù)執(zhí)行
     */
    function addPerson() {
      if (okNext) {
        okNext = false;
        personNum ++
        personNumP.innerHTML = `電梯人數(shù):${personNum}(假設(shè)電梯可以無限裝人)`
      }
      clearTimeout(timeoutFn);
      timeoutFn = setTimeout(function () {
        okNext = true;
      }, 500)
    }
    /**
     * @method 重置
     */
    function reset() {
      personNum = 0;
      personNumP.innerHTML = '電梯人數(shù):0(假設(shè)電梯可以無限裝人)';
    }

    addBtn.addEventListener('click', addPerson);
    resetBtn.addEventListener('click', reset);
  </script>
</body>
</html>

上面代碼要是看不太明白,可以直接粘下去自己執(zhí)行以下看看是什么感覺,就知道是什么意思了。

代碼純我自己寫的,要是有不對的地方,請大佬指正啊

Throttle 節(jié)流

什么是節(jié)流

節(jié)流呢,也是我自己的理解,在連續(xù)觸發(fā)一個方法的某一時間段中,控制方法的執(zhí)行次數(shù)。

同樣舉個例子吧,一個地鐵進(jìn)站閘口,10秒進(jìn)一個人(10秒內(nèi)執(zhí)行一個方法),管這10秒中來了是5個人、10個人還是20個人,都只是進(jìn)一個人(從第一次觸發(fā)后10秒不管被觸發(fā)多少次都不會執(zhí)行,直到下一個10秒才會再執(zhí)行)。

如何實(shí)現(xiàn)呢??

時間戳

我們首先用時間戳來判斷前后的時間間隔,然后就可以知道我從上次執(zhí)行完這個方法過了多久,過了這么長時間,是不是已經(jīng)超過了自己規(guī)定的時長,如果時長超過了,我就可以再次執(zhí)行了

image
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>地鐵進(jìn)站</title>
</head>
<body>
  <button id="addBtn">進(jìn)站人數(shù)+1</button><button id="resetBtn">重置</button>
  <p id="personTotal">旅客總?cè)藬?shù):0</p>
  <p id="personNum">進(jìn)站人數(shù):0</p>
  <script>
    var personNum = 0; // 進(jìn)站人數(shù)
    var personTotal = 0; // 一共來了多少人
    var addBtn = document.getElementById('addBtn'); // 獲取添加人數(shù)按鈕
    var personNumP = document.getElementById('personNum'); // 獲取顯示人數(shù)的標(biāo)簽
    var personTotalP = document.getElementById('personTotal'); // 獲取顯示總?cè)藬?shù)的標(biāo)簽
    var resetBtn = document.getElementById('resetBtn'); // 獲取重置按鈕
    /**
     * @method 增加進(jìn)站人數(shù)
     * @description 每個時間間隔執(zhí)行的方法
     */
    function addPerson() {
      personNum ++;
      personNumP.innerHTML = `進(jìn)站人數(shù):${personNum}`;
    }
    /**
     * @method 節(jié)流方法(時間戳)
     * @param {Function} fn 需要節(jié)流的實(shí)際方法
     * @param {Number} wait 需要控制的時間長度
     * @description 根據(jù)上一次執(zhí)行的時間,和這一次執(zhí)行的時間做比較,如果大于控制的時間,就可以執(zhí)行
     */
    function throttle(fn, wait) {
      var prev = 0; // 第一次執(zhí)行的時候是0,所以第一次點(diǎn)擊的時候肯定大于這個數(shù),所以會立馬執(zhí)行
      return function () {
        var context = this;
        var args = arguments;
        var now = Date.now(); // 實(shí)際執(zhí)行的時間
        personTotal ++;
        personTotalP.innerHTML = `旅客總?cè)藬?shù):${personTotal}`;
        if (now - prev >= wait) { // 執(zhí)行的時間是不是比上次執(zhí)行的時間大于需要延遲的時間,大于,我們就執(zhí)行
          fn.apply(context, args);
          prev = now; // 執(zhí)行了以后,重置上一次執(zhí)行的時間為剛剛執(zhí)行這次函數(shù)的時間,下次執(zhí)行就用這個時間為基準(zhǔn)
        }
      }
    }
    /**
     * @method 重置
     */
    function reset() {
      personNum = 0;
      personTotal = 0;
      personNumP.innerHTML = '進(jìn)站人數(shù):0';
      personTotalP.innerHTML = `旅客總?cè)藬?shù):0`;
    }

    addBtn.addEventListener('click', throttle(addPerson, 1000));
    resetBtn.addEventListener('click', reset);
  </script>
</body>
</html>

節(jié)流函數(shù)throttle用到了作用域,call、apply和閉包等相關(guān)的知識,看不懂的可以看我之前的文章

  1. 《前端戰(zhàn)五渣學(xué)JavaScript——閉包》
  2. 《前端戰(zhàn)五渣學(xué)JavaScript——call、apply以及bind》

上面的代碼中我感覺可以很直觀的看出來是根據(jù)判斷前后兩次的時間,來得知可不可以進(jìn)行下一次函數(shù)的執(zhí)行。參考著代碼中的注釋我覺得應(yīng)該可以看明白吧??????

setTimeout

如果我們用setTimeout的話,我們只需要更改一下throttle方法

image
/**
 * @method 節(jié)流方法(setTimeout)
 * @param {Function} fn 需要節(jié)流的實(shí)際方法
 * @param {Number} wait 需要控制的時間長度
 * @description 這個方法就很類似防抖了,就是判斷當(dāng)前函數(shù)有沒有延遲setTimeout函數(shù),有的話就不執(zhí)行了
 */
function throttle(fn, wait) {
  var timeout = null; 
  return function () {
    var context = this;
    var args = arguments;
    personTotal ++;
    personTotalP.innerHTML = `旅客總?cè)藬?shù):${personTotal}`;
    if (!timeout) { 
      var that = this;
      timeout = setTimeout(() => {
        timeout = null;
        fn.apply(context, args)
      }, wait)
    }
  }
}

雖然我們只需要更改幾行代碼就實(shí)現(xiàn)了用setTimeout實(shí)現(xiàn)節(jié)流的這個方法,但是我們仔細(xì)看上面的圖,我們可以發(fā)現(xiàn),當(dāng)我點(diǎn)擊第一次的時候,進(jìn)站旅客是沒有增加的,這跟我們實(shí)際情況不一樣,我們先來的,我不用等啊,我直接就能進(jìn)站,對不對。還有當(dāng)我結(jié)束增加人數(shù)的時候,進(jìn)站旅客過去等待時間以后還會加一個人,這當(dāng)然也不是我們想看到的。

使用時間戳還是setTimeout,取決于業(yè)務(wù)場景了

rAF(requestAnimationFrame)

誒??rAF是什么?什么是requestAnimationFrame?這在我沒有寫這篇博客的時候,我根本不知道window下還有個這個方法,神奇吧,那這個方法是干什么的呢??

告訴瀏覽器——你希望執(zhí)行一個動畫,并且要求瀏覽器在下次重繪之前調(diào)用指定的回調(diào)函數(shù)更新動畫。該方法需要傳入一個回調(diào)函數(shù)作為參數(shù),該回調(diào)函數(shù)會在瀏覽器下一次重繪之前執(zhí)行。————《MDN Web Docs》

就是在用這個可以一直重繪動畫,然后讓人看起來是個動畫,重繪的這個過程是個很頻繁的操作,所以如果我們自己寫,不加以干涉,在性能和資源上會造成嚴(yán)重的浪費(fèi),所以我們可以使用requestAnimationFrame來使用我們的動畫看起來很流暢,又不會頻繁調(diào)用

優(yōu)點(diǎn)

  1. 目標(biāo)是60fps(16毫秒的一幀),瀏覽器將決定如何安排渲染的最佳時間。
  2. 相對簡單和標(biāo)準(zhǔn)的API,未來不會改變,減少維護(hù)成本。

缺點(diǎn)

  1. rAF是內(nèi)部api,所以我們并不方便修改
  2. 如果瀏覽器選項(xiàng)卡沒有激活,就用不了
  3. 兼容性不好,在IE9,Opera Mini和舊Android中仍然不支持
  4. node中不能使用

讓我們來使用rAF吧

直接上圖

image
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>rAF使用</title>
  <style>
    #SomeElementYouWantToAnimate {
      width: 100px;
      height: 100px;
      background-color: #000;
    }
  </style>
</head>
<body>
  <div id="SomeElementYouWantToAnimate"></div>
  <script>
    var start = null;
    var element = document.getElementById('SomeElementYouWantToAnimate');
    element.style.position = 'absolute';
    /**
     * @method 移動我們的小黑方塊
     */
    function step(timestamp) {
      if (!start) start = timestamp;
      var progress = timestamp - start;
      element.style.left = Math.min(progress / 10, 200) + 'px';
      if (progress < 2000) {
        window.requestAnimationFrame(step);
      }
    }

    window.requestAnimationFrame(step);
  </script>
</body>
</html>

總結(jié)

rAF是一個內(nèi)部api,固定的16毫秒執(zhí)行一次,因?yàn)槿搜劢邮?0fps的動畫就會感到很流暢了,如果我們需要改變rAF的執(zhí)行時間,那我們只能自己去寫動畫的方法,節(jié)流還是防抖,看個人愛好了

收官

防抖:連續(xù)觸發(fā)一個函數(shù),不管是觸發(fā)開始執(zhí)行還是結(jié)束執(zhí)行,只要在連續(xù)觸發(fā),就只執(zhí)行一次

節(jié)流:規(guī)定時間內(nèi)只執(zhí)行一次,不管是規(guī)定時間內(nèi)被觸發(fā)了多少次

rAF:也算是一種節(jié)流手段,原生api,旨在使動畫在盡量少占用資源的情況下使動畫流暢

End Game

《復(fù)仇者聯(lián)盟4》現(xiàn)階段的漫威宇宙的結(jié)束,《哈利·波特》《火影忍者》一個個完結(jié)的電影,雖然在時刻提醒著我們青春再慢慢的消失,正如英雄聯(lián)盟中的那句話,我們有了新的敵人叫“生活”。當(dāng)這些完結(jié)的并不是真正的結(jié)束,《哈利·波特》有《神奇動物在哪里》,《火影忍者》有《博人傳》,《鋼鐵俠》有《蜘蛛俠》,晚輩從前輩手中接過接力棒,繼續(xù)往后跑,我們也從自己青蔥的歲月進(jìn)入下一階段,努力奮斗吧??!

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容