不定期更新的源碼閱讀日常——lodash-1

  1. 不定期更新的源碼閱讀日常將不會採用逐行摘抄源碼然後分析閱讀的方式進行源碼閱讀,而是提煉分享源碼中個人發人深省的部分進行摘錄總結,知識補足。
  2. 不定期更新的源碼閱讀日常閱讀的庫都是模塊零碎化或者小功能庫。方便靈活,而且不需要連續閱讀。
  3. 不定期更新的源碼閱讀日常將不定期更新。
  4. 歡迎大家關注我的個人博客,來查看我每週都會更新的一些文章。

今天我們來讀lodash的Array部分。

數組length的邊界處理

  lodash中Array部分相關操作,經常需要對入參的取值進行邊界處理。比如調用fill方法中startend的大小,chunk方法中size的大小,以確保函數的正常執行。

  在處理數組的length邊界時,lodash藉助位操作符,僅用一行代碼,保證了數組length在0之上,最大值範圍之下,我們借baseSlice方法中的一段源碼來學習一下。

/**
  * @param {Array} array The array to slice.
  * @param {number} [start=0] The start position.
  * @param {number} [end=array.length] The end position.
  * @returns {Array} Returns the slice of `array`.
*/
function baseSlice(array, start, end) {
    // ....省略不關鍵部分
    length = start > end ? 0 : ((end - start) >>> 0);
    start >>>= 0;
    var result = Array(length);
    // ....省略不關鍵部分
}

  baseSlice功能和目前Array自帶的slice方法功能相同,截取數組startend部分,返回截取的新數組。截取的代碼部分正在進行是根據startend的差值長度,生成新的數組對象,後面以便循環推入數據並返回結果。

  baseSlicelength根據startend的差值做了一個邊界處理。當startend小時,直接判length爲0;當endstart大時,取end - start的差,並做了一個>>>位運算符號,並且在後續,對start做了一個>>>=的操作處理。

  要想知道如此處理的原因,首先需要知道Array.length的邊界規定,我們引用一下mdn上關於Array.length的定義。

length 是Array的實例屬性。返回或設置一個數組中的元素個數。該值是一個無符號 32-bit 整數,並且總是大於數組最高項的下標。

  無符號 32-bit 整數意味着32-bit都可以用來進行數據的儲存,而不需要勻第一位出來作爲正負符號的標記。因此數組的長度範圍應該在0 ~ Math.pow(2, 32) - 1長度之間。而在不知道傳入endstart大小的情況下,length的長度實際上是有可能超出這個長度的。

  我們接着來看>>>操作的定義:

a >>> b將 a 的二進制表示向右移 b (< 32) 位,丟棄被移出的位,並使用 0 在左側填充。該操作符會將第一個操作數向右移動指定的位數。向右被移出的位被丟棄,左側用0填充。因爲符號位變成了 0,所以結果總是非負的。(譯註:即便右移 0 個比特,結果也是非負的。)

      9 (base 10): 00000000000000000000000000001001 (base 2)
                   --------------------------------
9 >>> 2 (base 10): 00000000000000000000000000000010 (base 2) = 2 (base 10)

  因此,baseSlice使用length >>> 0的方式保證了length的長度永遠在32-bit的範圍。即當數字大於2的32次方時候,>>>會崛棄所有大於32-bit的位數部分,即減去Math.pow(2, 32)。而小於範圍的數字由於位移的是0則不受任何影響。之後對start也做了一個確保,是因爲baseSlice需要截取start這一位到end爲止的數組數據,start的數字必須也要確保在length的範圍內。

調用優化

  在difference一系列方法源碼的時候,lodash都使用baseRest引導使用的函數重新綁定了作用域到lodash_上。而在baseRest中,都統一調用了一個setToString方法,它能讓傳入的函數都擁有一個toString方法,調用能夠直接看到傳入函數的函數體,即看到該函數的代碼。這在後續的一些需要傳入函數的方法中方便使用者調試起到了非常重要的作用。

/**
  * The base implementation of `_.rest` which doesn't validate or coerce arguments.
  * @param {Function} func The function to apply a rest parameter to.
  * @param {number} [start=func.length-1] The start position of the rest parameter.
  * @returns {Function} Returns the new function.
  */
function baseRest(func, start) {
    return setToString(overRest(func, start, identity), func + '');
}
/**
  * Sets the `toString` method of `func` to return `string`.
  *
  * @private
  * @param {Function} func The function to modify.
  * @param {Function} string The `toString` result.
  * @returns {Function} Returns `func`.
  */
var setToString = shortOut(baseSetToString);

  但我重點關注的其實是shortOut這個函數的代碼,很有意思,我們來看一下源碼:

/** Used to detect hot functions by number of calls within a span of milliseconds. */
var HOT_COUNT = 800,
    HOT_SPAN = 16;

/**
  * Creates a function that'll short out and invoke `identity` instead
  * of `func` when it's called `HOT_COUNT` or more times in `HOT_SPAN`
  * milliseconds.
  * @param {Function} func The function to restrict.
  * @returns {Function} Returns the new shortable function.
  */
function shortOut(func) {
    var count = 0,
    lastCalled = 0;

    return function() {
        // nativeNow 即 Date.now
        var stamp = nativeNow(),
            remaining = HOT_SPAN - (stamp - lastCalled);

        lastCalled = stamp;
        if (remaining > 0) {
          if (++count >= HOT_COUNT) {
            return arguments[0];
          }
        } else {
          count = 0;
        }
        return func.apply(undefined, arguments);
    };
}

  該方法實際上是使用了一個閉包包裹了一下傳入的函數,記錄下了函數調用次數count以及上次調用時間lastCalled。並針對這兩個數值,對常用函數調用做了一個調用限制的優化。

  我們可以看到,在每次調用函數前,這個方法都會利用Date.now去記錄一下當前調用的時間,並且和**上一次調動該函數時間(lastCalled)**進行一個比較。當這個差值大於HOT_SPAN(當前版本是16,即16ms)的時候,使用apply調用並清空調用次數(count)爲0。當差值小於HOT_SPAN,即兩次函數調用之間時間小於HOT_SPAN,而且調用次數大於HOT_COUNT(當前版本爲800,即800次),就停止調用該函數,而是返回函數入參的第一項,根據註釋,這第一項應該是一個函數的identity

  上面有提到過,在諸如setToString這樣的報錯機制處理時,使用了shortOut方法進行一個高階函數的包裝。setToString這個函數本身就是爲了服務lodash的一些報錯機制,讓傳入的函數都能擁有得到函數體代碼的toString方法,這樣可以保證在大批量數據處理的時候,根據不同的性能情況,進行不同的容錯處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章