- 不定期更新的源码阅读日常将不会采用逐行摘抄源码然后分析阅读的方式进行源码阅读,而是提炼分享源码中个人发人深省的部分进行摘录总结,知识补足。
- 不定期更新的源码阅读日常阅读的库都是模块零碎化或者小功能库。方便灵活,而且不需要连续阅读。
- 不定期更新的源码阅读日常将不定期更新。
今天我们来读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
方法,这样可以保证在大批量数据处理的时候,根据不同的性能情况,进行不同的容错处理。