0%

lodash源码解析:uniq和union家族

本篇继续分析下 uniqunion 家族的方法,uniq 方法主要是将数组去重,union 方法主要是将多个数组聚合成一个数组并去重。包括uniquniqByuniqWithunionunionByunionWith、以及核心方法baseUniqbashFlatten。并说明下在 lodashbywith这两个后缀的区别。

对应源码分析已推到 github 仓库: https://github.com/MageeLin/lodash-source-code-analysis

uniqunion 家族方法的依赖路径图如下所示,其实只用看左半部分,右半部分的代码已经在之前的文章中分析过。

lodash 中的后缀

lodash 的方法中,同一家族的方法都是用后缀来区别开的,比如本篇中的 uniqByuniqWith。仔细翻了下所有的方法,最常用的后缀就是下表这几个:

后缀 意义
by iteratee(value),迭代方法
with comparator(arrVal, othVal),比较方法
while predicate(value, index, array),断言方法
last fromRight,指示是否从右向左
right fromRight,指示是否从右向左
deep depthCLONE_DEEP_FLAG,指示是否深度运算

这里面大部分的后缀还是很容易看明白它的作用的,但是迭代方法 by 和比较方法 with 就很难分明白。多分析了几个方法,把自己的理解分享下。

就拿 uniq 方法举例,同样都是去重,uniqByuniqWith 的官方示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
// uniqBy的官方示例
uniqBy([2.1, 1.2, 2.3], Math.floor);
// => [2.1, 1.2]

// uniqWith的官方示例
const objects = [
{ x: 1, y: 2 },
{ x: 2, y: 1 },
{ x: 1, y: 2 },
];
uniqWith(objects, isEqual);
// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]

很显然,uniqBy 的示例中,是把数组中所有的元素都向下取整后进行了对应的去重,也就是说挨个比较了 Math.floor(2.1)Math.floor(1.2)Math.floor(2.3)相同的就对应去重.

而在 uniqWith 中,是调用了 isEqual 进行的两两比较,也就是说挨个比较 isEqual(objects[0], objects[1])isEqual(objects[0], objects[2])isEqual(objects[1], objects[2])返回true的就对应去重。

所以在我看来,with 是可以完全实现所有的 by 的,比如 uniqBy 的官方示例可以使用 uniqWith 来实现。

1
2
3
4
5
6
7
8
9
10
// uniqBy的官方示例
uniqBy([2.1, 1.2, 2.3], Math.floor);
// => [2.1, 1.2]

// 用uniqWith实现
uniqWith(
[2.1, 1.2, 2.3],
(arrVal, othVal) => Math.floor(arrVal) === Math.floor(othVal)
);
// => [2.1, 1.2]

uniq 和 union 家族

uniqunion 家族放在一起分析,是因为 union 的实现是利用的先将多个数组展平,然后再去重来实现的,所以 union 家族的三个方法都依赖到了 baseUniq

依赖的内部方法

setToArray

set 转化为 array

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 将 `set` 的值转化为 `array`
*
* @private
* @param {Object} set 要转化的 `set`
* @returns {Array} 返回 `values` 组成的数组
*/
function setToArray(set) {
let index = -1;
// 根据set的长度创建一个定长数组
const result = new Array(set.size);

// 不断的把set的值push给result
set.forEach((value) => {
result[++index] = value;
});
// 返回结果
return result;
}

export default setToArray;

createSet

创建一个 set,主要是在创建 set 做了兼容性的判断,很巧妙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import setToArray from './setToArray.js';

/** 作为各种`Number`常量的引用 */
const INFINITY = 1 / 0;

/**
* 创建一个`values`组成的set对象
*
* @private
* @param {Array} values 要添加到set中的values值
* @returns {Object} 返回新的set
*/
// [,-0] => [empty, -0]
// new Set([,-0]) => Set{ 0: undefined, 1: 0 }
// setToArray(new Set([,-0])) => [undefined, 0]
// setToArray(new Set([,-0]))[1] => 0
// 1 / setToArray(new Set([,-0]))[1] => Infinity
// 所以可以看出,这里第一个目的是来试一试 undefined 会不会被 Set 跳过,第二个目的是试一试-0会不会被转为0
// 用来判断浏览器对Set的支持程度
const createSet =
Set && 1 / setToArray(new Set([, -0]))[1] == INFINITY
? // 可以用set时,就返回创建set的函数
(values) => new Set(values)
: // 不能用set,就返回个空函数
() => {};

export default createSet;

baseUniq

baseUniq 实现时也进行了长数组的优化,如果是手动实现可以不用这么复杂,根本原理就是双层的迭代。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
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';

/** 超过该常量后,就进行大数组优化 */
const LARGE_ARRAY_SIZE = 200;

/**
* `uniqBy`的基础实现
*
* @private
* @param {Array} array 要检查的数组
* @param {Function} [iteratee] iteratee迭代器调用每个元素
* @param {Function} [comparator] comparator比较器比较每个元素
* @returns {Array} 返回一个新的数组副本
*/
function baseUniq(array, iteratee, comparator) {
// 初始化变量
let index = -1;
let includes = arrayIncludes;
let isCommon = true;

const { length } = array;
const result = [];
// 把seen的指向result的地址
let seen = result;

// 当第三个参数comparator存在时,就非普通模式
// 并把includes设为arrayIncludesWith
if (comparator) {
isCommon = false;
includes = arrayIncludesWith;
}
// 另外如果length过长,也是非普通模式
// 就用cache缓存的模式
else if (length >= LARGE_ARRAY_SIZE) {
const set = iteratee ? null : createSet(array);
if (set) {
return setToArray(set);
}
isCommon = false;
includes = cacheHas;
seen = new SetCache();
}
// 普通模式下,就指向result
else {
seen = iteratee ? [] : result;
}

// 跳出的位置
// 开始迭代
outer: while (++index < length) {
let value = array[index];
// 如果有迭代器,就用迭代器来变换下本次迭代的元素
const computed = iteratee ? iteratee(value) : value;

// 当comparator不存在且value为0时,将value设为0
value = comparator || value !== 0 ? value : 0;

// 普通模式情况下
if (isCommon && computed === computed) {
let seenIndex = seen.length;
// 在这里实现了去重,如果seen中查到了与computed相同的元素,就跳出到最外层
while (seenIndex--) {
if (seen[seenIndex] === computed) {
continue outer;
}
}
// 有iteratee时,就给seen push一个转化后的
if (iteratee) {
seen.push(computed);
}
// 给结果数组push该值
result.push(value);
}
// 非普通模式下,就调用includes方法
else if (!includes(seen, computed, comparator)) {
// 当为缓存的情况下,就给seen push这个computed
if (seen !== result) {
seen.push(computed);
}
// 给结果数组push该值
result.push(value);
}
}
return result;
}

export default baseUniq;

baseFlatten

baseFlatten 之前解析过,不赘述它的依赖了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import isFlattenable from './isFlattenable.js';

/**
* `flatten`方法的基本方法,支持带约束条件的扁平化
*
* @private
* @param {Array} array 要扁平化的数组
* @param {number} depth 最大的递归深度
* @param {boolean} [predicate=isFlattenable] 每次迭代调用的断言函数
* @param {boolean} [isStrict] 仅限通过`predicate`断言函数检查的值。
* @param {Array} [result=[]] 初始的结果数组
* @returns {Array} 返回一个新的扁平化后的数组。
*/
function baseFlatten(array, depth, predicate, isStrict, result) {
// 给predicate和result置初始值
// 默认是 isFlattenable 和 []
predicate || (predicate = isFlattenable);
result || (result = []);

// array为null或undefined时,返回result
if (array == null) {
return result;
}

// 开始迭代
for (const value of array) {
// 深度大于0且可扁平化的value才执行下一步
if (depth > 0 && predicate(value)) {
if (depth > 1) {
// 递归展平数组(受到调用堆栈数限制)。
baseFlatten(value, depth - 1, predicate, isStrict, result);
} else {
// 达到深度或完全展平后就push到result中
result.push(...value);
}
// 不可扁平化的值就按原样赋值给结果数组(isStrict为假的情况下)
} else if (!isStrict) {
// 这里其实处理的是不能通过predicate检验的元素,当isStrict为true的时候,就直接舍弃
// 相当于push,不使用push是因为性能原因
// 参见https://segmentfault.com/q/1010000021808718
result[result.length] = value;
}
}
// 把最后的result数组返回
return result;
}

export default baseFlatten;

uniq 家族

uniq 家族的实现都是利用的 baseUniq,分别是调用如下方法:

  1. baseUniq(array)
  2. baseUniq(array, iteratee)
  3. baseUniq(array, undefined, comparator)

uniq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import baseUniq from './.internal/baseUniq.js';

/**
* 创建一个去重后的array数组副本。
* 使用了 [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* 做等值比较。只有第一次出现的元素才会被保留。值在array中的顺序决定result的顺序。
*
* @since 0.1.0
* @category Array
* @param {Array} array 要检查的数组。
* @returns {Array} 返回一个去重后的新的array副本。
* @see uniqBy, uniqWith
* @example
*
* uniq([2, 1, 2])
* // => [2, 1]
*/
function uniq(array) {
// array有值且为数组时,返回baseUniq()的结果
return array != null && array.length ? baseUniq(array) : [];
}

export default uniq;

uniqBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import baseUniq from './.internal/baseUniq.js';

/**
* 这个方法类似 `uniq` ,但是它接受一个 `iteratee` (迭代函数),
* 调用数组(array)的每个元素以产生唯一性计算的标准。
* 值在array中的顺序决定result的顺序。
* iteratee 调用时会传入一个参数:(value)。
*
* @since 4.0.0
* @category Array
* @param {Array} array 要检查的array。
* @param {Function} iteratee iteratee调用每个元素。
* @returns {Array} 返回一个去重后的新的array副本。
* @see uniq, uniqWith
* @example
*
* uniqBy([2.1, 1.2, 2.3], Math.floor)
* // => [2.1, 1.2]
*/
function uniqBy(array, iteratee) {
// array有值且为数组时,返回baseUniq()的结果
return array != null && array.length ? baseUniq(array, iteratee) : [];
}

export default uniqBy;

uniqWith

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import baseUniq from './.internal/baseUniq.js';

/**
* 这个方法类似 `uniq`, 但是它接受一个 `comparator`比较器,
* 调用比较`array`数组的每一个元素。
* 值在array中的顺序决定result的顺序。
* comparator 调用时会传入2个参数: (arrVal, othVal)。
*
* @since 4.0.0
* @category Array
* @param {Array} array 要检查的数组。
* @param {Function} [comparator] comparator 调用每个元素。
* @returns {Array} 返回一个去重后的新的array副本。
* @see uniq, uniqBy
* @example
*
* const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }]
*
* uniqWith(objects, isEqual)
* // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
*/
function uniqWith(array, comparator) {
// 检查comparator是不是一个函数
comparator = typeof comparator === 'function' ? comparator : undefined;
return array != null && array.length
? // 调用baseUniq()
baseUniq(array, undefined, comparator)
: [];
}

export default uniqWith;

union 家族

union 家族的实现都是利用的 baseUniqbaseFlatten,分别是调用如下方法:

  1. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true))
  2. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), iteratee)
  3. baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), undefined, comparator)

union

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';

/**
* 创建一个按顺序排列的唯一值的数组。
* 所有给定数组的元素值使用
* [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
* 做等值比较。
* (注: arrays(数组)的并集,按顺序返回,返回数组的元素是唯一的)
*
* @since 0.1.0
* @category Array
* @param {...Array} [arrays] 要检查的arrays
* @returns {Array} 返回arrays的并集
* @see difference, unionBy, unionWith, without, xor, xorBy
* @example
*
* union([2, 3], [1, 2])
* // => [2, 3, 1]
*/
function union(...arrays) {
// 调用baseUniq(baseFlatten())
return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true));
}

export default union;

unionBy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';
import last from './last.js';

/**
* 这个方法类似 `union` ,但是它接受一个 `iteratee` (迭代函数),
* 调用`arrays`数组中的每个`array`的每个元素以产生唯一性计算的标准。
* 结果值从该值出现的第一个 `array` 中选择。
* iteratee 会传入一个参数:(value)。
*
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] 要检查的arrays。
* @param {Function} iteratee 每个元素调用的iteratee。
* @returns {Array} 返回组合values后的新数组。
* @see difference, union, unionWith, without, xor, xorBy
* @example
*
* unionBy([2.1], [1.2, 2.3], Math.floor)
* // => [2.1, 1.2]
*/
function unionBy(...arrays) {
// 拿到iteratee参数并确保是个函数
let iteratee = last(arrays);
if (isArrayLikeObject(iteratee)) {
iteratee = undefined;
}
// 调用baseUniq(baseFlatten(), iteratee)
return baseUniq(baseFlatten(arrays, 1, isArrayLikeObject, true), iteratee);
}

export default unionBy;

unionWith

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import baseFlatten from './.internal/baseFlatten.js';
import baseUniq from './.internal/baseUniq.js';
import isArrayLikeObject from './isArrayLikeObject.js';
import last from './last.js';

/**
* 这个方法类似 `union`但是它接受一个 `comparator`比较器 ,
* 调用比较 `arrays` 数组的每个 `array` 的每一个元素。
* 结果值从该值出现的第一个 `array` 中选择。
* comparator 调用时会传入2个参数: (arrVal, othVal)。
*
* @since 4.0.0
* @category Array
* @param {...Array} [arrays] The arrays to inspect.要检查的arrays。
* @param {Function} [comparator] `comparator`比较器调用每个元素。
* @returns {Array} 返回组合values后的新数组。
* @see difference, union, unionBy, without, xor, xorBy
* @example
*
* const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]
* const others = [{ 'x': 1, 'y': 1 }, { 'x': 1, 'y': 2 }]
*
* unionWith(objects, others, isEqual)
* // => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 1 }]
*/
function unionWith(...arrays) {
// comparator是最后一个参数,并确保是一个函数
let comparator = last(arrays);
comparator = typeof comparator === 'function' ? comparator : undefined;
// 调用baseUniq(baseFlatten(), undefined, comparator)
return baseUniq(
baseFlatten(arrays, 1, isArrayLikeObject, true),
undefined,
comparator
);
}

export default unionWith;
👆 全文结束,棒槌时间到 👇