- 不定期更新的源碼閱讀日常將不會採用逐行摘抄源碼然後分析閱讀的方式進行源碼閱讀,而是提煉分享源碼中個人發人深省的部分進行摘錄總結,知識補足。
- 不定期更新的源碼閱讀日常閱讀的庫都是模塊零碎化或者小功能庫。方便靈活,而且不需要連續閱讀。
- 不定期更新的源碼閱讀日常將不定期更新。
- 歡迎大家關注我的個人博客,來查看我每週都會更新的一些文章。
今天我們來讀lodash的Array部分。
數組length的邊界處理
lodash中Array部分相關操作,經常需要對入參的取值進行邊界處理。比如調用fill方法中start
和end
的大小,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
方法功能相同,截取數組start
到end
部分,返回截取的新數組。截取的代碼部分正在進行是根據start
和end
的差值長度,生成新的數組對象,後面以便循環推入數據並返回結果。
baseSlice
對length
根據start
和end
的差值做了一個邊界處理。當start
比end
小時,直接判length
爲0;當end
比start
大時,取end - start
的差,並做了一個>>>
位運算符號,並且在後續,對start做了一個>>>=
的操作處理。
要想知道如此處理的原因,首先需要知道Array.length的邊界規定,我們引用一下mdn
上關於Array.length的定義。
length 是Array的實例屬性。返回或設置一個數組中的元素個數。該值是一個無符號 32-bit 整數,並且總是大於數組最高項的下標。
無符號 32-bit 整數
意味着32-bit
都可以用來進行數據的儲存,而不需要勻第一位出來作爲正負符號的標記。因此數組的長度範圍應該在0 ~ Math.pow(2, 32) - 1
長度之間。而在不知道傳入end
和start
大小的情況下,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
方法,這樣可以保證在大批量數據處理的時候,根據不同的性能情況,進行不同的容錯處理。