亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專(zhuān)欄INFORMATION COLUMN

【源碼分析】給你幾個(gè)鬧鐘,或許用 10 分鐘就能寫(xiě)出 lodash 中的 debounce &

余學(xué)文 / 2934人閱讀

摘要:最簡(jiǎn)單的案例以最簡(jiǎn)單的情景為例在某一時(shí)刻點(diǎn)只調(diào)用一次函數(shù),那么將在時(shí)間后才會(huì)真正觸發(fā)函數(shù)。后續(xù)我們會(huì)逐漸增加黑色鬧鐘出現(xiàn)的復(fù)雜度,不斷去分析紅色鬧鐘的位置。

相比網(wǎng)上教程中的 debounce 函數(shù),lodash 中的 debounce 功能更為強(qiáng)大,相應(yīng)的理解起來(lái)更為復(fù)雜;

解讀源碼一般都是直接拿官方源碼來(lái)解讀,不過(guò)這次我們采用另外的方式:從最簡(jiǎn)單的場(chǎng)景開(kāi)始寫(xiě)代碼,然后慢慢往源碼上來(lái)靠攏,循序漸進(jìn)來(lái)實(shí)現(xiàn) lodash 中的 debounce 函數(shù),從而更深刻理解官方 debounce 源碼的用意

為了減少純代碼帶來(lái)的晦澀感,本文以圖例來(lái)輔助講解,一方面這樣能減少源碼閱讀帶來(lái)的枯燥感,同時(shí)也讓后續(xù)回憶源碼內(nèi)容更加的具體形象。(記住圖的內(nèi)容,后續(xù)再寫(xiě)出源碼也變得簡(jiǎn)單些)

在本文的末尾還會(huì)附上簡(jiǎn)易的 debounce & throttle 的實(shí)現(xiàn)的代碼片段,方便平時(shí)快速用在簡(jiǎn)單場(chǎng)景中,免去引用 lodash 庫(kù)。

本文屬于源碼解讀類(lèi)型的文章,對(duì) debounce 還不熟悉的讀者建議先通過(guò)參考文章(在文末)了解該函數(shù)的概念和用法。
1、用圖例解析 debounce 源碼
附源碼 debounce: https://github.com/boycgit/ts...

首先搬出 debounce(防抖)函數(shù)的概念:函數(shù)在 wait 秒內(nèi)只執(zhí)行一次,若這 wait 秒內(nèi),函數(shù)高頻觸發(fā),則會(huì)重新計(jì)算時(shí)間。

看似簡(jiǎn)單一句話,內(nèi)含乾坤。為方便行文敘述,約定如下術(shù)語(yǔ):

假定我們要對(duì) func 函數(shù)進(jìn)行 debounce 處理,經(jīng) debounced 后的返回值我們稱(chēng)之為 debounced func

wait 表示傳入防抖函數(shù)的時(shí)間

time 表示當(dāng)前時(shí)間戳

lastCallTime 表示上一次調(diào)用 debounced func 函數(shù)的時(shí)間

lastInvokeTime 表示上一次 func 函數(shù)執(zhí)行的時(shí)間

result 是每次調(diào)用 debounced func 函數(shù)的返回值

time 表示當(dāng)前時(shí)間

本文將搭配圖例 + 程序代碼的方式,將上述概念具象到圖中。

2、最簡(jiǎn)單的案例

以最簡(jiǎn)單的情景為例:在某一時(shí)刻點(diǎn)只調(diào)用一次 debounced func 函數(shù),那么將在 wait 時(shí)間后才會(huì)真正觸發(fā) func 函數(shù)。

將這個(gè)情景形成一幅圖例,最終繪制出的圖如下所示:

下面我們?cè)敿?xì)講解這幅圖的產(chǎn)生過(guò)程,其實(shí)不難,基本上看一遍就懂。

首先繪制在圖中放置一個(gè)黑色鬧鐘表示用戶(hù)調(diào)用 debounced func 函數(shù):(同時(shí)用 lastCallTime 標(biāo)示出最近一次調(diào)用 debounced func 的時(shí)間)

同時(shí)在距離該黑色鬧鐘 wait 處放置一個(gè)藍(lán)色鬧鐘,表示setTimout(..., wait),該藍(lán)色鬧鐘表示未來(lái)當(dāng)代碼運(yùn)行到該時(shí)間點(diǎn)時(shí),需要做一些判斷:

為了標(biāo)示出表示程序當(dāng)前運(yùn)行的進(jìn)度(當(dāng)前時(shí)間戳),我們用橙紅色滑塊來(lái)表示:

當(dāng)紅色滑塊到達(dá)該藍(lán)色鬧鐘處的時(shí)候,藍(lán)色鬧鐘會(huì)進(jìn)行判斷:因?yàn)楫?dāng)前滑塊距離最近的黑色鬧鐘的時(shí)間差為 wait

故而做出判斷(依據(jù) debounce 函數(shù)的功能定義):需要觸發(fā)一次 func 函數(shù),我們用紅色鬧鐘來(lái)表示 func 函數(shù)的調(diào)用,所以就放置一個(gè)紅色鬧鐘

很顯然藍(lán)色和紅色鬧鐘重疊起來(lái)的。

同時(shí)我們給紅色鬧鐘標(biāo)上 lastInvokeTime,記錄最近一次調(diào)用 func 的時(shí)間:

注意 lastInvokeTimelastCallTime 的區(qū)別,兩者含義是不一樣的

這樣我們就完成了最簡(jiǎn)單場(chǎng)景下 debounce 圖例的繪制,簡(jiǎn)單易懂。

后續(xù)我們會(huì)逐漸增加黑色鬧鐘出現(xiàn)的復(fù)雜度,不斷去分析紅色鬧鐘的位置。這樣就能將理解 debounce 源碼的問(wèn)題轉(zhuǎn)換成“根據(jù)圖上黑色鬧鐘的位置,請(qǐng)畫(huà)出紅色鬧鐘位置”的問(wèn)題,而分析紅色鬧鐘位置的過(guò)程中也就是理解 debounce 源碼的過(guò)程;

用圖例方式輔助理解源碼的方式可以減少源碼閱讀帶來(lái)的枯燥感,同時(shí)后續(xù)回憶源碼內(nèi)容起來(lái)也更加具體形象。

為避免后續(xù)寫(xiě)文章到處解釋圖中元素的概念含義,這里不妨先羅列出來(lái),如果閱讀過(guò)程中忘記到這里回憶一下也會(huì)方便許多:

橫線代表時(shí)間軸,橙紅色滑塊代表當(dāng)前時(shí)間 time

每個(gè)黑色箭頭表示 debounced func 函數(shù)的調(diào)用

黑色鬧鐘表示調(diào)用 debounced func 函數(shù)時(shí)的時(shí)間,最后一次黑色鬧鐘上標(biāo)上 lastCallTime,表示最近一次調(diào)用的時(shí)間戳;

紅色鬧鐘表示調(diào)用 func 函數(shù)的時(shí)間,最后一次紅色鬧鐘上標(biāo)上 lastInvokeTime,表示最近一次調(diào)用的時(shí)間戳;

此外還有一個(gè)藍(lán)色鬧鐘,表示 setTimeout 時(shí)間戳(用來(lái)規(guī)劃 func 函數(shù)執(zhí)行的時(shí)間),每次時(shí)間軸上的橙紅色滑塊到這個(gè)時(shí)間點(diǎn)就要做判斷:是執(zhí)行 func 或者推遲藍(lán)色鬧鐘位置

有關(guān)藍(lán)色鬧鐘,這里有兩個(gè)注意點(diǎn):

時(shí)間軸上最多同時(shí)只有一個(gè)藍(lán)色鬧鐘;

只有在第一次調(diào)用 debounced func 函數(shù)時(shí)才會(huì)在 wait 時(shí)間后放置藍(lán)色鬧鐘,后續(xù)鬧鐘的出現(xiàn)位置就由藍(lán)色鬧鐘自己決策(下文會(huì)舉例說(shuō)明)

3、有 N 多個(gè)黑色鬧鐘的場(chǎng)景

現(xiàn)在我們來(lái)一個(gè)稍微復(fù)雜的場(chǎng)景:

假如在 wait 時(shí)間內(nèi)(記住這個(gè)前提條件)調(diào)用 n 次 debounced func 函數(shù),如下所示:

第一次調(diào)用 debounced func 函數(shù)會(huì)在 wait 時(shí)間后放置藍(lán)色鬧鐘(只有第一次調(diào)用會(huì)放置藍(lán)色鬧鐘,后續(xù)鬧鐘的位置由藍(lán)色鬧鐘自己決策):

以上就是描述,那么問(wèn)題來(lái)了:請(qǐng)問(wèn)紅色鬧鐘應(yīng)該出現(xiàn)在時(shí)間軸哪個(gè)位置?

3.1、分析紅色鬧鐘出現(xiàn)的位置

我們只關(guān)注最后一個(gè)黑色鬧鐘,并假設(shè)藍(lán)色鬧鐘距離該黑色鬧鐘時(shí)間間隔為 x

那么第一個(gè)黑色鬧鐘和最后一個(gè)黑色鬧鐘的時(shí)間間隔是 wait - x

接下來(lái)我們關(guān)注橙紅色滑塊(即當(dāng)前時(shí)間time)到達(dá)藍(lán)色鬧鐘的時(shí),藍(lán)色鬧鐘開(kāi)始做決策:計(jì)算可知 x < wait,此時(shí)藍(lán)色鬧鐘決定不放置紅色鬧鐘(即不觸發(fā) func),而是將藍(lán)色鬧鐘往后挪了挪,挪動(dòng)距離為 wait - x,調(diào)整完之后的藍(lán)色鬧鐘位置如下:

之所以挪 wait - x 的距離,是因?yàn)榕餐旰蟮乃{(lán)色鬧鐘距離最后一個(gè)黑色鬧鐘恰好為 wait 間隔(從而保證 debounce 函數(shù)至少間隔 wait 時(shí)間 才觸發(fā)的條件):

從挪移之后開(kāi)始,到下一次橙色鬧鐘再次遇到藍(lán)色鬧鐘這段期間,我們暫且稱(chēng)之為 ”藍(lán)色決策間隔期“(請(qǐng)忍耐這抽象的名稱(chēng),畢竟我想了好久),藍(lán)色鬧鐘基于此間隔期的內(nèi)容來(lái)進(jìn)行決策,只有兩種決策:

如果在”藍(lán)色決策間隔期“內(nèi)沒(méi)有黑鬧鐘出現(xiàn),那么紅色滑塊達(dá)到藍(lán)色鬧鐘的時(shí)候,藍(lán)色鬧鐘計(jì)算獲知當(dāng)前藍(lán)色鬧鐘距離上一個(gè)黑色鬧鐘的時(shí)間間隔不少于 waittime - lastCallTime >= wait),那就會(huì)放置紅色鬧鐘(即調(diào)用 func),目標(biāo)達(dá)成;

如果在”藍(lán)色決策間隔期“內(nèi)仍舊有黑鬧鐘出現(xiàn),那么當(dāng)橙紅色滑塊到達(dá)藍(lán)色鬧鐘時(shí),藍(lán)色鬧鐘又會(huì)重新計(jì)算與該間隔期內(nèi)最后一只黑色鬧鐘的距離 y,隨后 又會(huì)往后挪動(dòng)位置 wait-y,再一次保證藍(lán)色鬧鐘距離最后一個(gè)黑色鬧鐘恰好為 wait 間隔 —— 沒(méi)錯(cuò),又形成了新的 ”藍(lán)色決策間隔期“;那接下去的分析就又回到了 這里兩點(diǎn)(即遞歸決策),直到能放置到紅鬧鐘為止。

從上我們可以看到,藍(lán)色鬧鐘一直保持 ”紳士“ 風(fēng)范,隨著黑色鬧鐘的逼近,藍(lán)色鬧鐘一直保持”克制“態(tài)度,不斷調(diào)整自己的位置,讓調(diào)整后的位置總是和最后一個(gè)黑色鬧鐘保持 wait 的距離。

3.2、用代碼描述圖例過(guò)程

我們用代碼將上述的過(guò)程描述出來(lái),就是下面這個(gè)樣子:

function debounce(func, wait, options) {
  var lastArgs, lastThis, result, timerId, lastCallTime, lastInvokeTime = 0, trailing = true;
  
  wait = toNumber(wait) || 0;  

  // 紅色滑塊達(dá)到藍(lán)色鬧鐘時(shí),藍(lán)色鬧鐘根據(jù)條件作出決策
  function timerExpired() {
    var time = now();

    // 決策 1: 滿(mǎn)足放置紅色鬧鐘的條件,則放置紅鬧鐘
    if (shouldInvoke(time)) {
      return trailingEdge(time);
    }
    // 否則,決策 2:將藍(lán)色鬧鐘再往后挪 `wait-x` 位置,形成  ”藍(lán)色決策間隔期“
    timerId = setTimeout(timerExpired, remainingWait(time));
  }

  // === 以下是具體決策中的函數(shù)實(shí)現(xiàn) ==== 
   // 做出 ”應(yīng)當(dāng)放置紅色鬧鐘“ 的決策的條件:藍(lán)色鬧鐘和最后一個(gè)黑色鬧鐘的間隔不小于 wait 間隔
  function shouldInvoke(time) {
    var timeSinceLastCall = time - lastCallTime;
    return (
      timeSinceLastCall >= wait
    );
  }

  // 具體函數(shù):放置紅色鬧鐘
  function trailingEdge(time) {
    timerId = undefined;
    
    if (trailing && lastArgs) {
      return invokeFunc(time);
    }
    lastArgs = lastThis = undefined;
    return result;
  }
  // 具體函數(shù) - 子函數(shù):在時(shí)間軸上放置紅鬧鐘
  function invokeFunc(time) {
    var args = lastArgs,
      thisArg = lastThis;

    lastArgs = lastThis = undefined;
    lastInvokeTime = time;
    result = func.apply(thisArg, args);
    return result;
  }  
  
  // 具體函數(shù):計(jì)算讓藍(lán)色鬧鐘往后挪 wait-x 位置
  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeWaiting = wait - timeSinceLastCall;

    return timeWaiting ;
  }  
  // ==============


 // 主流程:讓紅色滑塊在時(shí)間軸上前進(jìn)(即 debounced func 函數(shù)的執(zhí)行)
 function debounced() {
    var time = now();
    lastArgs = arguments;
    lastThis = this;
    lastCallTime = time;

    if (timerId === undefined) {
      timerId = setTimeout(timerExpired, wait);
    }
    return result;
  }
  return debounced;
}

這部分代碼還請(qǐng)不要略過(guò),因?yàn)榇a是從debounce源碼中整理過(guò)來(lái),除了函數(shù)順序略有調(diào)整外,源碼風(fēng)格保持原有的,相當(dāng)于直接閱讀源碼。每個(gè)函數(shù)都有注釋?zhuān)瑢?duì)比著圖例閱讀下來(lái)相信讀完會(huì)有收獲的。

上述這份代碼已經(jīng)包含了 debounce 源碼的核心骨架,接下來(lái)我們繼續(xù)擴(kuò)展場(chǎng)景,將源碼內(nèi)容豐滿(mǎn)起來(lái)。

4、豐富功能特性 4.1、支持 leading 特性

leading 功能簡(jiǎn)單理解就是,在第一次(注意這個(gè)條件)放下黑色鬧鐘的時(shí)候:

立即放置紅鬧鐘,同時(shí)在

此后 wait 處放置方式藍(lán)色鬧鐘(注:第一次放下黑色鬧鐘的時(shí)候,按理說(shuō)也會(huì)在 wait 處放下藍(lán)色鬧鐘,考慮既然 leading 也有這種操作,那么就不多此一舉。記?。赫麄€(gè)時(shí)間軸上最多只能同時(shí)有一個(gè)藍(lán)色鬧鐘

用圖說(shuō)話:

第一次放置黑色鬧鐘的時(shí)候,會(huì)疊加上紅色鬧鐘(當(dāng)然這個(gè)紅色鬧鐘上會(huì)標(biāo)示 lastInvokeTime),另外在 wait 間隔后會(huì)有藍(lán)色鬧鐘。其他流程和之前案例分析一樣。

在代碼層面,我們給剛才的 debounce 函數(shù)添加 leading 功能(通過(guò) options.leading 開(kāi)啟)、新增一個(gè) leadingEdge 方法后,再微調(diào)剛才的代碼:

function debounce(func, wait, options) {
  ...
  
  var leading = false; // 默認(rèn)不開(kāi)啟
  leading = !!options.leading; // 通過(guò) options.leading 開(kāi)啟
  
  ...
  
  // 首先:新增執(zhí)行 leading 處的操作的函數(shù)
  function leadingEdge(time) {
    lastInvokeTime = time; // 設(shè)置 lastInvokeTime 時(shí)間標(biāo)簽
    timerId = setTimeout(timerExpired, wait); // 同時(shí)在此后 `wait` 處放置一個(gè)藍(lán)色鬧鐘
    return leading ? invokeFunc(time) : result; // 如果開(kāi)啟,直接放置紅色鬧鐘;否則直接返回 result 數(shù)值
  }
  ...
  
  // 其次:給放置紅色鬧鐘新增一種條件
   function shouldInvoke(time) {
    ...
    return (
      lastCallTime === undefined || // 初次執(zhí)行時(shí)
      timeSinceLastCall >= wait // 或者前面分析的條件,兩次 `debounced func` 調(diào)用間隔大于 wait 
    );
  }
  
   // 注意:放置完紅色鬧鐘后,記得要清空 timerId,相當(dāng)于清空時(shí)間軸上藍(lán)色鬧鐘;
  function trailingEdge(time) {
    timerId = undefined;
    ... 
  }
  
  // 最后:leading 邊界調(diào)用
  function debounced(){
    ...
    var isInvoking = shouldInvoke(time); //  判斷是否可以放置紅色鬧鐘
    
    ...
    
    if (isInvoking) { // 如果可以放置紅色鬧鐘
      
      if (timerId === undefined) { // 且當(dāng)時(shí)間軸上沒(méi)有藍(lán)色鬧鐘
        // 執(zhí)行 leading 邊界處操作(放置紅色鬧鐘 或 直接返 result)
        return leadingEdge(lastCallTime);
      }
      
    }
    
    ...
    return result;
  }

  return debounced;
}
4.2、支持 maxWait 特性

要理解這個(gè) maxWait 特性,我們先看一種特殊情況,在 {leading: false} 下, 時(shí)間軸上我們很密集地放置黑色鬧鐘:

按之前的所述規(guī)則,我們的藍(lán)色鬧鐘一直保持紳士態(tài)度,隨著黑色鬧鐘的逼近,藍(lán)色鬧鐘將不斷將調(diào)整自己的位置,讓自己調(diào)整后的位置總是和最后一個(gè)黑色鬧鐘保持 wait 的距離:

那么在這種情況下,如果黑色鬧鐘一直保持這種密集放置狀態(tài),理論上就紅色鬧鐘就沒(méi)有機(jī)會(huì)出現(xiàn)在時(shí)間軸上。

那在這種情況下能否實(shí)現(xiàn)一個(gè)功能,無(wú)論黑色鬧鐘多么密集,時(shí)間軸上最多隔 maxWait 時(shí)間就出現(xiàn)紅色鬧鐘,就像下圖那樣:

有了這個(gè)功能屬性后,藍(lán)色鬧鐘從此 ”變得堅(jiān)強(qiáng)“,也有了 "底線",縱使黑色鬧鐘的不斷逼近,也會(huì)堅(jiān)守 maxWait 底線,到點(diǎn)就放置紅色鬧鐘。

實(shí)現(xiàn)該特性的大致思路如下:

maxWait 是與 lastInvokeTime 共同協(xié)作

在藍(lán)色鬧鐘計(jì)算后退距離時(shí),maxWait 發(fā)揮作用;在沒(méi)有 maxWait 的時(shí)候,是按上一次黑色鬧鐘進(jìn)行測(cè)距,保證調(diào)整后的藍(lán)色鬧鐘和黑色鬧鐘保持 wait 的距離;而在有了 maxWait 后,藍(lán)色鬧鐘調(diào)整距離還會(huì)考慮上一次紅色鬧鐘的位置,保持調(diào)整后鬧鐘的位置和紅色鬧鐘距離不能超過(guò) maxWait,這就是底線了,到了一定程度,就算黑色鬧鐘在逼近,藍(lán)色鬧鐘也不會(huì) ”退縮“:

從代碼層面上看, maxWait 具體是在 remainingWait 方法 和 shouldInvoke 中發(fā)揮作用的:

function debounce(func, wait, options) {
  ...
  
  var lastInvokeTime = 0; // 初始化
  var maxing = false; // 默認(rèn)沒(méi)有底線
  
  maxing = "maxWait" in options;
  maxWait = maxing
      ? nativeMax(toNumber(options.maxWait) || 0, wait)
      : maxWait; // 從 options.maxWait 中獲取底線數(shù)值
  
  ...
  // 首先,在在藍(lán)色鬧鐘決策后退多少距離時(shí),maxWait 發(fā)揮了作用
  function remainingWait(time) {
    var timeSinceLastCall = time - lastCallTime,
      timeSinceLastInvoke = time - lastInvokeTime,
      timeWaiting = wait - timeSinceLastCall;

    // 在這里發(fā)揮作用,保持底線
    return maxing
      ? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
      : timeWaiting;
  }
  
  ...

  
  // 其次:針對(duì) `maxWait`,給放置紅色鬧鐘新增一種可能條件
   function shouldInvoke(time) {
    ...
    var timeSinceLastInvoke = time - lastInvokeTime; // 獲取距離上一次紅色鬧鐘的時(shí)間間隔
    return (
      lastCallTime === undefined || // 初次執(zhí)行時(shí)
      timeSinceLastCall >= wait ||  // 或者前面分析的條件,兩次 `debounced func` 調(diào)用間隔大于 wait 
      (maxing && timeSinceLastInvoke >= maxWait) // 兩次紅色鬧鐘間隔超過(guò) maxWait
    );
  }
  
  
  // 最后:leading 邊界調(diào)用
  function debounced(){
    ...
    var isInvoking = shouldInvoke(time); //  判斷是否可以放置紅色鬧鐘的條件
    
    ...
    
    if (isInvoking) { // 如果可以放置紅色鬧鐘
      
      ...
      // 邊界情況的處理,保證在緊 loop 中能正常保持觸發(fā)
      if (maxing) {
        timerId = setTimeout(timerExpired, wait);
        return invokeFunc(lastCallTime);
      }
      
    }
    
    ...
    return result;
  }

  return debounced;
}

因此,maxWait 能夠讓紅色鬧鐘保證在 maxWait 間隔內(nèi)至少出現(xiàn) 1 次;

4.3、支持 cancel / flush 方法

這兩個(gè)函數(shù)是為了能隨時(shí)控制 debounce 的緩存狀態(tài);

其中 cancel 方法源碼如下:

 //  取消防抖
  function cancel() {
    if (timerId !== undefined) {
      clearTimeout(timerId);
    }
    lastInvokeTime = 0;
    lastArgs = lastCallTime = lastThis = timerId = undefined;
  }

調(diào)用該方法,相當(dāng)于直接在時(shí)間軸上去除藍(lán)色鬧鐘,這樣紅色方塊(時(shí)間)就永遠(yuǎn)遇見(jiàn)不了藍(lán)色鬧鐘,那樣也就不會(huì)有放置紅色鬧鐘的可能了。

其中 flush 方法源碼如下:

function flush() {
  return timerId === undefined ? result : trailingEdge(now());
}

非常直觀,調(diào)用該方法相當(dāng)于直接在時(shí)間軸上放置紅色鬧鐘。

至此,我們已經(jīng)完整實(shí)現(xiàn)了 lodash 的 debounce 函數(shù),也就相當(dāng)于閱讀了一遍其源碼。

5、實(shí)現(xiàn) throttle 函數(shù)

在完成上面 debounce 功能和特性后(尤其是 maxWait 特性),就能借助 debounce 實(shí)現(xiàn) throttle 函數(shù)了。

看 throttle 源碼 就能明白:

function throttle(func, wait, options) {
  var leading = true,
      trailing = true;
  // ...
  return debounce(func, wait, {
    "leading": leading,
    "maxWait": wait,
    "trailing": trailing
  });
}

所以在 lodash 中,只需要 debounce 函數(shù)即可,throttle 相當(dāng)于 ”充話費(fèi)“ 送的。

至此,我們已經(jīng)解讀完 lodash 中的 debounce & throttle 函數(shù)源碼;

最后附帶一張 lodash 實(shí)現(xiàn)執(zhí)行效果圖,用來(lái)自測(cè)是否真的理解通透:

注:此圖取自于文章《 聊聊lodash的debounce實(shí)現(xiàn)》
6、小結(jié)

在前端領(lǐng)域的性能優(yōu)化手段中,防抖(debounce)和節(jié)流(throttle)是必備的技能,網(wǎng)上隨便一搜就有很多文章去分析解釋?zhuān)环?yōu)秀的文章使用 圖文混排 + 類(lèi)比方式 深入淺出探討這兩函數(shù)的用法和使用場(chǎng)景(見(jiàn)文末的參考文檔)。

那我為什么還要寫(xiě)這一篇文章?

緣起前兩天手動(dòng)將 lodash 中的 debouncethrottle 兩個(gè)函數(shù) TS 化的需求,而平時(shí)我也只是使用并沒(méi)有在意它們真正的實(shí)現(xiàn)原理,因此在遷移過(guò)程我順帶閱讀了一番 lodash 中這兩個(gè)函數(shù)的源碼。

具體原因和遷移過(guò)程請(qǐng)移步《技巧 - 快速 TypeScript 化 lodash 中的 throttle & debounce 函數(shù)》

本文嘗試提供了另一個(gè)視角去解讀,通過(guò)時(shí)間軸 + 鬧鐘圖例 + 代碼的方式來(lái)解讀 lodash 中的 debounce & throttle 源碼;
整個(gè)流程下來(lái)只要理解了黑色、藍(lán)色、紅色這 3 種鬧鐘的關(guān)系,那么憑著理解力去實(shí)現(xiàn)簡(jiǎn)版 lodashdebounce 函數(shù)并非難事。

當(dāng)然上述的敘述中,略過(guò)了很多細(xì)節(jié)和存在性的判斷(諸如 timeId 的存在性判斷、isInvoking的出現(xiàn)位置等),省略這主要是為了降低源碼閱讀的難度;(實(shí)際中這些細(xì)節(jié)的處理有時(shí)候反而很重要,是代碼健壯性不可或缺的一部分)

希望本文能對(duì)讀者理解 lodash 中的 debounce & throttle 源碼有些許的幫助,歡迎隨時(shí)關(guān)注微信公眾號(hào)或者技術(shù)博客留言交流。

【附】代碼片段

如果在你僅僅需要應(yīng)付簡(jiǎn)單的一些場(chǎng)景,也可以直接使用下方的代碼片段。

A. 簡(jiǎn)易 debounce - 只實(shí)現(xiàn) trailing 情況

防抖函數(shù)的概念:函數(shù)在 n 秒內(nèi)只執(zhí)行一次,若這 n 秒內(nèi),函數(shù)高頻觸發(fā),則會(huì)重新計(jì)算時(shí)間。

將這段話翻譯成代碼,你會(huì)發(fā)現(xiàn)并不難:

//防抖代碼最簡(jiǎn)單的實(shí)現(xiàn)
function debounce(func, wait) {
  let timerId, result;

  return function() {
    if(timerId){
      clearTimeout(timerId);  //  每次觸發(fā) 都清除當(dāng)前timer,重新設(shè)置時(shí)間
    }
    
    timerId = setTimeout(function(){
     result = func.apply(this, arguments);
    }, wait);
    
    return result;
  }
}

debounce 返回閉包(匿名函數(shù))

假如調(diào)用該閉包兩次:

如果調(diào)用兩次間隔 < wait 數(shù)值,先前調(diào)用會(huì)被 clearTimeout ,也就不執(zhí)行;最終只執(zhí)行 1 次調(diào)用(即第 2 次的調(diào)用)

如果調(diào)用兩次間隔 > wait 數(shù)值,當(dāng)執(zhí)行 clearTimeout 的時(shí)候,前一次調(diào)用已經(jīng)執(zhí)行了;所以最終這兩次調(diào)用都會(huì)執(zhí)行

上述的實(shí)現(xiàn),是最經(jīng)典的 trailing 情況,即以 wait 間隔結(jié)束點(diǎn)作為函數(shù)調(diào)用計(jì)時(shí)點(diǎn),是我們平時(shí)用的最多的場(chǎng)景

B. 簡(jiǎn)易 debounce - 只實(shí)現(xiàn) leading 功能

另外用得比較多的就是以 wait 間隔開(kāi)始點(diǎn)作為函數(shù)調(diào)用計(jì)時(shí)點(diǎn),即 leading 功能。

將上面代碼中最后的 setTimeout 內(nèi)容改成 timerId = undefined ,而將 fn.apply 提取出來(lái)加個(gè) if 條件語(yǔ)句就行 ,修改后代碼如下:

//防抖代碼最簡(jiǎn)單的實(shí)現(xiàn)
function debounce(func, wait) {
  let timerId, result;

  return function() {
    if(timerId){
      clearTimeout(timerId);  //  每次觸發(fā) 都清除當(dāng)前timer,重新設(shè)置時(shí)間
    }
    
    if(!timerId){
      result = fn.apply(this, arguments);
    }
    
    timerId = setTimeout(function() {
        timerId = undefined;
    }, wait);
    
    return result;
  }
}
fn.apply(lastThis, lastArgs) 之所以用 if 條件包裹,是針對(duì)首次調(diào)用的邊界情況

debounce 仍舊返回閉包(匿名函數(shù))

timerId 是閉包變量,相當(dāng)于標(biāo)志位,通過(guò)它可以知道某個(gè)函數(shù)的調(diào)用是否在上一次函數(shù)調(diào)用的影響范圍內(nèi)

假如調(diào)用該閉包兩次:

如果調(diào)用兩次間隔 < wait 數(shù)值,后調(diào)用因?yàn)槿栽谇耙淮蔚?wait 影響范圍內(nèi),所以會(huì)被 clearTimeout 掉;最終只執(zhí)行 1 次調(diào)用(即第 1 次的調(diào)用)

如果調(diào)用兩次間隔 > wait 數(shù)值,當(dāng)執(zhí)行第二次時(shí) timerId 已經(jīng)是 underfined 的,所以會(huì)立即執(zhí)行 函數(shù),所以最終這兩次調(diào)用都會(huì)執(zhí)行

C. 簡(jiǎn)易 throttle 函數(shù)

throttle 函數(shù)的概念:函數(shù)在 n 秒內(nèi)只執(zhí)行一次,若這 n 秒內(nèi)還在有函數(shù)調(diào)用的請(qǐng)求都直接被忽略掉

實(shí)現(xiàn)原理也很簡(jiǎn)單:定義開(kāi)關(guān)變量 canRun,在定時(shí)開(kāi)啟的這段時(shí)間內(nèi)控制這個(gè)開(kāi)關(guān)變量為canRun = false上鎖),執(zhí)行完后才讓 canRun = true 即可。

  function throttle(func, wait) {
    let canRun = true
    return function () {
      if (!canRun) {
        return  // 如果開(kāi)關(guān)關(guān)閉了,那就直接不執(zhí)行下邊的代碼
      }
      canRun = false // 持續(xù)觸發(fā)的話,run一直是false,就會(huì)停在上邊的判斷那里
      setTimeout(() => {
        func.apply(this, arguments)
        canRun = true // 定時(shí)器到時(shí)間之后,會(huì)把開(kāi)關(guān)打開(kāi),我們的函數(shù)就會(huì)被執(zhí)行
      }, wait)
    }
  }
參考文章

Debouncing and Throttling Explained Through Examples:首推這篇經(jīng)典的文章,本文詳細(xì)描述了 lodash 中的 debounce 和 throttle 的思路設(shè)計(jì);里面使用 圖文混排 深入淺出探討這兩函數(shù)的用法和具體使用場(chǎng)景,更為難得還嵌入有可交互 demo,能即刻感受這兩方法的具體使用方式;嫌看英文麻煩的可以看中文版 《實(shí)例解析防抖動(dòng)(Debouncing)和節(jié)流閥(Throttling)》

防抖(debounce)函數(shù)的作用是什么?有哪些應(yīng)用場(chǎng)景,請(qǐng)實(shí)現(xiàn)一個(gè)防抖函數(shù):討論帖子,里面有不少的相關(guān)信息和資源

淺談 Underscore.js 中 _.throttle 和 _.debounce 的差異:很不錯(cuò)的釋義文章,電梯類(lèi)比秒懂

lodash.debounce: lodash debounce 多帶帶的庫(kù),附官方文檔

防抖(debounce)函數(shù)的作用是什么:解釋了 debounce 函數(shù)的原理和實(shí)現(xiàn)

聊聊lodash的debounce實(shí)現(xiàn):作者對(duì)比了自己的實(shí)現(xiàn)和 lodash 中的實(shí)現(xiàn)

Confused about the maxWait option for LoDash’s debounce method:解釋 ‘maxWait’ 的作用

第 3 題:什么是防抖和節(jié)流?有什么區(qū)別?如何實(shí)現(xiàn):面試題,簡(jiǎn)單快速實(shí)現(xiàn)防抖和節(jié)流這兩個(gè)函數(shù)

函數(shù)的防抖和節(jié)流是個(gè)啥???:用通俗的例子講解這兩個(gè)概念和實(shí)現(xiàn)

從lodash源碼學(xué)習(xí)節(jié)流與防抖:詳細(xì)注釋 lodash 中的 debounce 函數(shù)的實(shí)現(xiàn)

下面的是我的公眾號(hào)二維碼圖片,歡迎關(guān)注交流。

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/105977.html

相關(guān)文章

  • 快速 TypeScript 化 lodash 中的 throttle &amp; debounce

    摘要:背景需要包寫(xiě)起來(lái)爽,然而如果遇到?jīng)]有現(xiàn)成的化的工具函數(shù),就需要自己想辦法弄出一份類(lèi)型聲明文件了。最為重要的是,這種遷移方面我們可以隨意自定義化中所需要的工具函數(shù),遷移粒度都可以由自己控制。 1、背景 1.1、需要 TS 包 TypeScript 寫(xiě)起來(lái)爽,然而如果遇到?jīng)]有現(xiàn)成的 TS 化的工具函數(shù),就需要自己想辦法弄出一份類(lèi)型聲明文件了。 前兩天要寫(xiě)的小工具庫(kù)(Typescript 語(yǔ)...

    lewinlee 評(píng)論0 收藏0
  • JavaScript Debounce&amp;Throttle

    摘要:如果我們的回調(diào)函數(shù)較為復(fù)雜,頁(yè)面的性能就會(huì)變差。而可以保證穩(wěn)定的時(shí)間間隔執(zhí)行一次回調(diào)函數(shù)。但需要弄清楚的是,無(wú)論是還是,控制的都是回調(diào)函數(shù)的執(zhí)行,而不是事件的監(jiān)聽(tīng)。 前言 假設(shè)現(xiàn)在有個(gè)需求:監(jiān)聽(tīng)滑動(dòng)事件,并執(zhí)行回調(diào)。當(dāng)你用觸摸板或者鼠標(biāo)滑動(dòng)頁(yè)面時(shí),每秒鐘大概會(huì)觸發(fā)幾十次scroll事件,而當(dāng)你在手機(jī)等移動(dòng)終端上滑動(dòng)頁(yè)面時(shí),每秒就會(huì)觸發(fā)一百次scroll事件。如果我們的回調(diào)函數(shù)較為復(fù)雜,...

    The question 評(píng)論0 收藏0
  • 一次發(fā)現(xiàn)underscore源碼bug的經(jīng)歷以及對(duì)學(xué)術(shù)界拿來(lái)主義的思考

    摘要:事情是如何發(fā)生的最近干了件事情,發(fā)現(xiàn)了源碼的一個(gè)。樓主找到的關(guān)于和區(qū)別的資料如下關(guān)于拿來(lái)主義為什么這么多文章里會(huì)出現(xiàn)澤卡斯的錯(cuò)誤代碼樓主想到了一個(gè)詞,叫做拿來(lái)主義。的文章,就深刻抨擊了拿來(lái)主義這一現(xiàn)象。 事情是如何發(fā)生的 最近干了件事情,發(fā)現(xiàn)了 underscore 源碼的一個(gè) bug。這件事本身并沒(méi)有什么可說(shuō)的,但是過(guò)程值得我們深思,記錄如下,各位看官仁者見(jiàn)仁智者見(jiàn)智。 平時(shí)有瀏覽別...

    Lionad-Morotar 評(píng)論0 收藏0
  • 【譯】通過(guò)例子解釋 Debounce 和 Throttle

    摘要:舉例舉例通過(guò)拖拽瀏覽器窗口,可以觸發(fā)很多次事件。不支持,所以不能在服務(wù)端用于文件系統(tǒng)事件??偨Y(jié)將一系列迅速觸發(fā)的事件例如敲擊鍵盤(pán)合并成一個(gè)單獨(dú)的事件。確保一個(gè)持續(xù)的操作流以每毫秒執(zhí)行一次的速度執(zhí)行。 Debounce 和 Throttle 是兩個(gè)很相似但是又不同的技術(shù),都可以控制一個(gè)函數(shù)在一段時(shí)間內(nèi)執(zhí)行的次數(shù)。 當(dāng)我們?cè)诓僮?DOM 事件的時(shí)候,為函數(shù)添加 debounce 或者 th...

    LeoHsiun 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<