lodash原始碼分析之陣列的差集

对角另一面發表於2018-03-14

外部世界那些破舊與貧困的樣子,可以使我內心世界得到平衡。

——卡爾維諾《煙雲》

本文為讀 lodash 原始碼的第十七篇,後續文章會更新到這個倉庫中,歡迎 star:pocket-lodash

gitbook也會同步倉庫的更新,gitbook地址:pocket-lodash

作用與用法

baseDifference 可以用來獲取指定陣列與另一個陣列的差集。

這個函式是內部函式,是後面實現其它比較函式的核心函式。

baseDifference 的方法簽名如下:

baseDifference(array, values, iteratee, comparator)
複製程式碼

第一和第二個引數是需要比較的兩個陣列;iteratee 可以返回一值對映值,比較時,可以使用對映的值來進行比較; comparator 是自定義比較函式,如果有傳遞,則呼叫自定義的比較函式來進行交集的比較。

依賴

import SetCache from './SetCache.js'
import arrayIncludes from './arrayIncludes.js'
import arrayIncludesWith from './arrayIncludesWith.js'
import map from '../map.js'
import cacheHas from './cacheHas.js'
複製程式碼

lodash原始碼分析之快取使用方式的進一步封裝

lodash原始碼分析之arrayIncludes

lodash原始碼分析之arrayIncludesWith

lodash原始碼分析之map的實現

lodash原始碼分析之cacheHas

原始碼分析

const LARGE_ARRAY_SIZE = 200
function baseDifference(array, values, iteratee, comparator) {
  let includes = arrayIncludes
  let isCommon = true
  const result = []
  const valuesLength = values.length

  if (!array.length) {
    return result
  }
  if (iteratee) {
    values = map(values, (value) => iteratee(value))
  }
  if (comparator) {
    includes = arrayIncludesWith
    isCommon = false
  }
  else if (values.length >= LARGE_ARRAY_SIZE) {
    includes = cacheHas
    isCommon = false
    values = new SetCache(values)
  }
  outer:
  for (let value of array) {
    const computed = iteratee == null ? value : iteratee(value)

    value = (comparator || value !== 0) ? value : 0
    if (isCommon && computed === computed) {
      let valuesIndex = valuesLength
      while (valuesIndex--) {
        if (values[valuesIndex] === computed) {
          continue outer
        }
      }
      result.push(value)
    }
    else if (!includes(values, computed, comparator)) {
      result.push(value)
    }
  }
  return result
}
複製程式碼

iteratee的呼叫

if (iteratee) {
  values = map(values, (value) => iteratee(value))
}
複製程式碼

如果有傳遞 iteratee ,則先呼叫 map ,使用 iteratee 生成要比較陣列的對映陣列 values

因為後面會有巢狀迴圈,避免重複呼叫 iteratee ,影響效能,所以一開始就需要生成 values 的對映陣列。

效能優化

這裡使用了 isCommon 來標誌是否使用普通方式來處理。

if (comparator) {
  includes = arrayIncludesWith
  isCommon = false
}
複製程式碼

如果有傳遞比較函式,則將 isCommon 標記為 false,表示不用普通的方式來處理,後面可以看到,最後會使用 includes 方法來處理,也即 arrayIncludesWith 方法。

else if (values.length >= LARGE_ARRAY_SIZE) {
  includes = cacheHas
  isCommon = false
  values = new SetCache(values)
}
複製程式碼

如果不需要使用自定義的比較方式,並且陣列較大時(這裡限定了200),則使用 SetCache 類來快取陣列。

SetChche 其實使用的是 Map/Set 或者物件的方式來儲存,避免大陣列巢狀迴圈時造成的效能損耗。

迴圈比較

接下來就遍歷第一個陣列 array,將陣列中的每一項和第二個陣列的每一項比較。

if (isCommon && computed === computed) {
  let valuesIndex = valuesLength
  while (valuesIndex--) {
    if (values[valuesIndex] === computed) {
      continue outer
    }
  }
  result.push(value)
}
else if (!includes(values, computed, comparator)) {
  result.push(value)
}
複製程式碼

可以看到,如果 isCommon 沒有標記為 false, 或者需要比較的值 computed 不為 NaN 時,都採用巢狀迴圈的方式來比較。迴圈完畢,沒有在第二個陣列中發現相同的項時,將該項存入陣列 result 中。

如果 isCommonfalse 或者需要比較的值為 NaN 時,則呼叫 includes 方法來比較。

由之前的分析得知:

  • 如果指定 comparator ,則 includesarrayIncludesWith
  • 如果被比較的陣列 values 的長度超過 200 ,則 includescacheHas
  • 否則,includesarrayIncludes

+0與-0的處理

在看程式碼的時候,有一段十分奇怪:

value = (comparator || value !== 0) ? value : 0
複製程式碼

這段程式碼的意思是,在沒有提供 comparator 的情況下,如果 value === 0 ,則將 value 賦值為 0

value === 0 時,可能為 +0-00 ,lodash 為什麼要將它們都轉為 0 呢?

後來看到 lodash 作者在 issue 中說,因為比較會用到 Set ,而 Set 是不能區分 +0-0 的。

參考

Lodash系列——difference函式原始碼解析

value = (comparator || value !== 0) ? value : 0; does it work?

License

署名-非商業性使用-禁止演繹 4.0 國際 (CC BY-NC-ND 4.0)

最後,所有文章都會同步傳送到微信公眾號上,歡迎關注,歡迎提意見:

lodash原始碼分析之陣列的差集

作者:對角另一面

相關文章