We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
本文为读 lodash 源码的第九十八篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash
gitbook也会同步仓库的更新,gitbook地址:pocket-lodash
import SetCache from './SetCache.js' import arrayIncludes from './arrayIncludes.js' import arrayIncludesWith from './arrayIncludesWith.js' import cacheHas from './cacheHas.js' import createSet from './createSet.js' import setToArray from './setToArray.js'
《lodash源码分析之缓存使用方式的进一步封装》 《lodash源码分析之arrayIncludes》 《lodash源码分析之arrayIncludesWith》 《lodash源码分析之cacheHas》 《lodash源码分析之createSet》 《lodash源码分析之setToArray》
baseUniq 的作用是数组去重,是实现 uniq 、uniqBy 和 uniqWith 的内部方法,因此除了需要支持基本的去重操作外,还要支持 uniqBy 的 iteratee 参数和 uniqWith 的 comparator 参数。
baseUniq
uniq
uniqBy
uniqWith
iteratee
comparator
所有源码如下:
const LARGE_ARRAY_SIZE = 200 function baseUniq(array, iteratee, comparator) { let index = -1 let includes = arrayIncludes let isCommon = true const { length } = array const result = [] let seen = result if (comparator) { isCommon = false includes = arrayIncludesWith } else if (length >= LARGE_ARRAY_SIZE) { const set = iteratee ? null : createSet(array) if (set) { return setToArray(set) } isCommon = false includes = cacheHas seen = new SetCache } else { seen = iteratee ? [] : result } outer: while (++index < length) { let value = array[index] const computed = iteratee ? iteratee(value) : value value = (comparator || value !== 0) ? value : 0 if (isCommon && computed === computed) { let seenIndex = seen.length while (seenIndex--) { if (seen[seenIndex] === computed) { continue outer } } if (iteratee) { seen.push(computed) } result.push(value) } else if (!includes(seen, computed, comparator)) { if (seen !== result) { seen.push(computed) } result.push(value) } } return result }
在 es6 中,Set 的 key 是唯一的,因此在支持 Set 的环境下,可以通过先将传入的 array 转换成 Set 结构,再将 Set 转换回数组,即可达成去重的目的。
es6
Set
key
array
假如,baseUniq 只有一个参数,没有传迭代器 iteratee 和比较器 comparator,也即在最简单的情况下,是可以直接用这种方式来转换的。
相关的代码如下:
if (comparator) { ... } else if (length >= LARGE_ARRAY_SIZE) { const set = iteratee ? null : createSet(array) if (set) { return setToArray(set) } }
为什么要在 length >= LARGE_ARRAY_SIZE 才采用这种方式呢?其实这个主要不是针对 Set 转换的,而是为了性能优化,在数组长度比较长的时候,会用到缓存,后面会分析到。
length >= LARGE_ARRAY_SIZE
如果不用 Set,要将数组去重要怎样做呢?
我们可以用一个数组 result 来放置去重后的结果,然后一个一个遍历原数组 array,在每次迭代时,检测当前值是否已经在结果集 result 中,如果没有在 result 中,则将当前值存入 result 中即可,否则直接跳过。
result
根据这个思路,很容易写出以下的代码:
let index = -1 const { length } = array const result = [] outer: while(++index < length) { let value = array[index] value = value !== 0 ? value : 0 let seenIndex = result.length while (seenIndex--) { if (result[seenIndex] === value) { continue outer } } result.push(value) } return result
因为正负0 的存在,所以在 value 为 +0 或者 -0 时,都转换成 0 。
0
value
+0
-0
但是这段代码还是有问题的,这里没有考虑到 value 为 NaN 的情况,我们知道,在 js 中 NaN === NaN 是 false ,所以这段代码返回的结果中,可能会有多个 NaN 的存在。
NaN
js
NaN === NaN
false
那这种情况要怎样比较呢?可以用 lodash 的内部方法 arrayIncludes,这个方法考虑到了 NaN 的情况。
lodash
arrayIncludes
代码改成如下:
let index = -1 let includes = arrayIncludes const { length } = array const result = [] outer: while(++index < length) { let value = array[index] value = value !== 0 ? value : 0 if (value === value) { let seenIndex = result.length while (seenIndex--) { if (result[seenIndex] === value) { continue outer } } result.push(value) } else if (!includes(result, value)) { result.push(value) } } return result
在传入迭代器时,在进行比较的时候,会使用 iteratee 返回的值来进行比较。
这个要改起来也很简单,只需要将获取 value 的地方调用一下 iteratee 即可。
因为要比较的值和最后返回的值不一致,这时每次迭代的时候,就不能直接用 result 是否存在 value 来判断了,必须要用另外一个容器 seen 来保存 iteratee 返回的值。
seen
let index = -1 let includes = arrayIncludes const { length } = array const result = [] let seen = result if (comparator) { ... } else if (length >= LARGE_ARRAY_SIZE) { ... } else { seen = iteratee ? [] : result } outer: while (++index < length) { let value = array[index] const computed = iteratee ? iteratee(value) : value value = value !== 0 ? value : 0 if (computed === computed) { let seenIndex = seen.length while (seenIndex--) { if (seen[seenIndex] === computed) { continue outer } } if (iteratee) { seen.push(computed) } result.push(value) } else if (!includes(seen, computed)) { if (seen !== result) { seen.push(computed) } result.push(value) } } return result
可以看到,在没有传 iteratee 的时候,seen 和 result 是同一个引用,即可上一节的逻辑完全一致。
在传入比较器的时候,就需要用到 arrayIncludesWith 的方法了,因为 arrayIncludes 并不接受 comparator 参数,而且必须要确保迭代的时候,每次都走入调用 arrayIncludesWith 函数的分支,这可以用一个状态位 isCommon 来表示。
arrayIncludesWith
isCommon
... if (comparator) { isCommon = false includes = arrayIncludesWith } else if (length >= LARGE_ARRAY_SIZE) { ... } else { ... } outer: while (++index < length) { ... value = (comparator || value !== 0) ? value : 0 if (isCommon && computed === computed) { ... } else if (!includes(seen, computed, comparator)) { ... } } return result
isCommon 默认为 true ,在传入 comparator 的时候,isCommon 改为 false,并且将 incldues 设置为 arrayIncludesWith ,这样每次迭代的时候都会走调用 includes 的分支。
true
incldues
includes
另外值得留意的是,如果有传 comparator ,value 是不会做正负零转换的,完全交由 comparator 自己处理。
在数组比较大的时候,如果使用 arrayIncludes 或者 arrayIncludesWith 或者双重循环去比较,性能都比较差,因为时间复杂度会达到 O(n^2) 。
O(n^2)
这时可以用 SetCache 来优化性能。
SetCache
if (comparator) { ... } else if (length >= LARGE_ARRAY_SIZE) { isCommon = false includes = cacheHas seen = new SetCache } else { ... }
因为需要使用 SetCache ,需要使用 cacheHas 判断数据是否已经存在,因此需要设置 includes ,也就要求后面迭代 array 的时候,需要每次都走 includes 调用的分支,也即要将 isCommon 设置为 false 。
cacheHas
MDN: Set
lodash.js createSet函数的疑问
署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面
The text was updated successfully, but these errors were encountered:
No branches or pull requests
本文为读 lodash 源码的第九十八篇,后续文章会更新到这个仓库中,欢迎 star:pocket-lodash
gitbook也会同步仓库的更新,gitbook地址:pocket-lodash
依赖
《lodash源码分析之缓存使用方式的进一步封装》
《lodash源码分析之arrayIncludes》
《lodash源码分析之arrayIncludesWith》
《lodash源码分析之cacheHas》
《lodash源码分析之createSet》
《lodash源码分析之setToArray》
源码分析
baseUniq
的作用是数组去重,是实现uniq
、uniqBy
和uniqWith
的内部方法,因此除了需要支持基本的去重操作外,还要支持uniqBy
的iteratee
参数和uniqWith
的comparator
参数。所有源码如下:
使用Set去重
在
es6
中,Set
的key
是唯一的,因此在支持Set
的环境下,可以通过先将传入的array
转换成Set
结构,再将Set
转换回数组,即可达成去重的目的。假如,
baseUniq
只有一个参数,没有传迭代器iteratee
和比较器comparator
,也即在最简单的情况下,是可以直接用这种方式来转换的。相关的代码如下:
为什么要在
length >= LARGE_ARRAY_SIZE
才采用这种方式呢?其实这个主要不是针对Set
转换的,而是为了性能优化,在数组长度比较长的时候,会用到缓存,后面会分析到。整体思路
如果不用
Set
,要将数组去重要怎样做呢?我们可以用一个数组
result
来放置去重后的结果,然后一个一个遍历原数组array
,在每次迭代时,检测当前值是否已经在结果集result
中,如果没有在result
中,则将当前值存入result
中即可,否则直接跳过。根据这个思路,很容易写出以下的代码:
因为正负
0
的存在,所以在value
为+0
或者-0
时,都转换成0
。但是这段代码还是有问题的,这里没有考虑到
value
为NaN
的情况,我们知道,在js
中NaN === NaN
是false
,所以这段代码返回的结果中,可能会有多个NaN
的存在。那这种情况要怎样比较呢?可以用
lodash
的内部方法arrayIncludes
,这个方法考虑到了NaN
的情况。代码改成如下:
传入迭代器
iteratee
的情况在传入迭代器时,在进行比较的时候,会使用
iteratee
返回的值来进行比较。这个要改起来也很简单,只需要将获取
value
的地方调用一下iteratee
即可。因为要比较的值和最后返回的值不一致,这时每次迭代的时候,就不能直接用
result
是否存在value
来判断了,必须要用另外一个容器seen
来保存iteratee
返回的值。代码改成如下:
可以看到,在没有传
iteratee
的时候,seen
和result
是同一个引用,即可上一节的逻辑完全一致。传入比较器
comparator
的情况在传入比较器的时候,就需要用到
arrayIncludesWith
的方法了,因为arrayIncludes
并不接受comparator
参数,而且必须要确保迭代的时候,每次都走入调用arrayIncludesWith
函数的分支,这可以用一个状态位isCommon
来表示。isCommon
默认为true
,在传入comparator
的时候,isCommon
改为false
,并且将incldues
设置为arrayIncludesWith
,这样每次迭代的时候都会走调用includes
的分支。另外值得留意的是,如果有传
comparator
,value
是不会做正负零转换的,完全交由comparator
自己处理。大数组性能优化
在数组比较大的时候,如果使用
arrayIncludes
或者arrayIncludesWith
或者双重循环去比较,性能都比较差,因为时间复杂度会达到O(n^2)
。这时可以用
SetCache
来优化性能。因为需要使用
SetCache
,需要使用cacheHas
判断数据是否已经存在,因此需要设置includes
,也就要求后面迭代array
的时候,需要每次都走includes
调用的分支,也即要将isCommon
设置为false
。参考资料
MDN: Set
lodash.js createSet函数的疑问
License
署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面
The text was updated successfully, but these errors were encountered: