Skip to content
New issue

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源码分析之baseUniq #99

Open
HeftyKoo opened this issue Feb 29, 2020 · 0 comments
Open

lodash源码分析之baseUniq #99

HeftyKoo opened this issue Feb 29, 2020 · 0 comments
Labels
internal 内部函数或方法 系列文章

Comments

@HeftyKoo
Copy link
Owner

HeftyKoo commented Feb 29, 2020

本文为读 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 的作用是数组去重,是实现 uniquniqByuniqWith 的内部方法,因此除了需要支持基本的去重操作外,还要支持 uniqByiteratee 参数和 uniqWithcomparator 参数。

所有源码如下:

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 中,Setkey 是唯一的,因此在支持 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

但是这段代码还是有问题的,这里没有考虑到 valueNaN 的情况,我们知道,在 jsNaN === NaNfalse ,所以这段代码返回的结果中,可能会有多个 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 的时候,seenresult 是同一个引用,即可上一节的逻辑完全一致。

传入比较器 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 的分支。

另外值得留意的是,如果有传 comparatorvalue 是不会做正负零转换的,完全交由 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

参考资料

MDN: Set

lodash.js createSet函数的疑问

License

署名-非商业性使用-禁止演绎 4.0 国际 (CC BY-NC-ND 4.0)

最后,所有文章都会同步发送到微信公众号上,欢迎关注,欢迎提意见:

作者:对角另一面

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
internal 内部函数或方法 系列文章
Projects
None yet
Development

No branches or pull requests

1 participant