lodash源码分析之baseUniq
本文为读 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
参数。
所有源码如下:
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
}
使用Set去重
在 es6
中,Set
的 key
是唯一的,因此在支持 Set
的环境下,可以通过先将传入的 array
转换成 Set
结构,再将 Set
转换回数组,即可达成去重的目的。
假如,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
转换的,而是为了性能优化,在数组长度比较长的时候,会用到缓存,后面会分析到。
整体思路
如果不用 Set
,要将数组去重要怎样做呢?
我们可以用一个数组 result
来放置去重后的结果,然后一个一个遍历原数组 array
,在每次迭代时,检测当前值是否已经在结果集 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
。
但是这段代码还是有问题的,这里没有考虑到 value
为 NaN
的情况,我们知道,在 js
中 NaN === NaN
是 false
,所以这段代码返回的结果中,可能会有多个 NaN
的存在。
那这种情况要怎样比较呢?可以用 lodash
的内部方法 arrayIncludes
,这个方法考虑到了 NaN
的情况。
代码改成如下:
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
的情况
在传入迭代器时,在进行比较的时候,会使用 iteratee
返回的值来进行比较。
这个要改起来也很简单,只需要将获取 value
的地方调用一下 iteratee
即可。
因为要比较的值和最后返回的值不一致,这时每次迭代的时候,就不能直接用 result
是否存在 value
来判断了,必须要用另外一个容器 seen
来保存 iteratee
返回的值。
代码改成如下:
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
是同一个引用,即可上一节的逻辑完全一致。
传入比较器 comparator
的情况
在传入比较器的时候,就需要用到 arrayIncludesWith
的方法了,因为 arrayIncludes
并不接受 comparator
参数,而且必须要确保迭代的时候,每次都走入调用 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
的分支。
另外值得留意的是,如果有传 comparator
,value
是不会做正负零转换的,完全交由 comparator
自己处理。
大数组性能优化
在数组比较大的时候,如果使用 arrayIncludes
或者 arrayIncludesWith
或者双重循环去比较,性能都比较差,因为时间复杂度会达到 O(n^2)
。
这时可以用 SetCache
来优化性能。
if (comparator) {
...
}
else if (length >= LARGE_ARRAY_SIZE) {
isCommon = false
includes = cacheHas
seen = new SetCache
}
else {
...
}
因为需要使用 SetCache
,需要使用 cacheHas
判断数据是否已经存在,因此需要设置 includes
,也就要求后面迭代 array
的时候,需要每次都走 includes
调用的分支,也即要将 isCommon
设置为 false
。
参考资料
License
署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)
最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:
作者:对角另一面