Lodash 源码阅读-baseDifference
概述
baseDifference
是 Lodash 内部的一个基础函数,用于计算两个数组之间的差集。它支持自定义迭代器和比较器,能够高效处理各种复杂场景下的差集运算,是 Lodash 中 _.difference
、_.differenceBy
和 _.differenceWith
等公共方法的核心实现。
前置学习
依赖函数
- arrayIncludes:检查数组中是否包含指定值
- arrayIncludesWith:使用自定义比较器检查数组中是否包含某值
- arrayMap:对数组中的每个元素应用迭代器函数
- baseUnary:将函数转换为只接收一个参数的函数
- cacheHas:检查 SetCache 中是否存在某个值
- SetCache:一个专门用于存储唯一值的缓存结构
技术知识
- 数组遍历:高效遍历数组的实现方式
- 集合操作:如何实现差集操作
- 函数参数处理:处理可选参数的技巧
- 性能优化:大数组处理的优化策略
- NaN 处理:处理 JavaScript 中的 NaN 值比较问题
源码实现
js
function baseDifference(array, values, iteratee, comparator) {
var index = -1,
includes = arrayIncludes,
isCommon = true,
length = array.length,
result = [],
valuesLength = values.length;
if (!length) {
return result;
}
if (iteratee) {
values = arrayMap(values, baseUnary(iteratee));
}
if (comparator) {
includes = arrayIncludesWith;
isCommon = false;
} else if (values.length >= LARGE_ARRAY_SIZE) {
includes = cacheHas;
isCommon = false;
values = new SetCache(values);
}
outer: while (++index < length) {
var value = array[index],
computed = iteratee == null ? value : iteratee(value);
value = comparator || value !== 0 ? value : 0;
if (isCommon && computed === computed) {
var valuesIndex = valuesLength;
while (valuesIndex--) {
if (values[valuesIndex] === computed) {
continue outer;
}
}
result.push(value);
} else if (!includes(values, computed, comparator)) {
result.push(value);
}
}
return result;
}
实现思路
baseDifference
函数的核心思想是找出存在于第一个数组但不存在于第二个数组的所有元素。它通过以下几个步骤实现:
- 初始化阶段:设置默认参数和初始状态,创建结果数组
- 边界检查:对空数组进行快速处理
- 预处理输入:根据是否提供迭代器对值进行转换
- 优化检查策略:根据输入参数和数组大小选择最优的元素查找策略
- 遍历主数组:对每个元素判断是否存在于第二个数组中
- 构建结果:不存在于第二个数组的元素被添加到结果数组
函数通过智能地选择查找策略(直接遍历、使用比较器或使用 SetCache)来优化性能,特别是对大型数组的处理。
源码解析
1. 参数初始化
js
var index = -1,
includes = arrayIncludes,
isCommon = true,
length = array.length,
result = [],
valuesLength = values.length;
这段代码初始化了几个关键变量:
index = -1
:数组遍历指针(循环中使用++index
)includes = arrayIncludes
:默认使用的值检查函数isCommon = true
:标记是否使用简单比较模式length = array.length
:缓存第一个数组的长度result = []
:存储结果的数组valuesLength = values.length
:缓存第二个数组的长度
2. 边界条件处理
js
if (!length) {
return result;
}
如果第一个数组为空,直接返回空结果数组,因为空数组与任何数组的差集都是空数组。
3. 迭代器处理
js
if (iteratee) {
values = arrayMap(values, baseUnary(iteratee));
}
如果提供了迭代器函数,则使用 arrayMap
对第二个数组中的每个元素应用迭代器。baseUnary
确保迭代器函数总是只接收一个参数。
示例:
js
// 原始调用
_.differenceBy([2.1, 1.2], [2.3, 3.4], Math.floor);
// 转换后的 values 数组
// [2, 3],因为 Math.floor(2.3) = 2, Math.floor(3.4) = 3
4. 查找策略选择
js
if (comparator) {
includes = arrayIncludesWith;
isCommon = false;
} else if (values.length >= LARGE_ARRAY_SIZE) {
includes = cacheHas;
isCommon = false;
values = new SetCache(values);
}
这段代码根据输入参数和数据规模选择最优的元素查找策略:
- 如果提供了比较器 :
- 使用
arrayIncludesWith
执行自定义比较 - 设置
isCommon = false
表示不使用简单比较模式
- 使用
- 如果第二个数组较大 (元素数量超过 LARGE_ARRAY_SIZE,通常是 200):
- 使用
cacheHas
和SetCache
提高查找效率 - 设置
isCommon = false
表示不使用简单比较模式 - 将第二个数组转换为
SetCache
实例
- 使用
这种策略选择是为了优化性能:小数组使用直接遍历,大数组使用缓存结构降低时间复杂度。
5. 主遍历逻辑
js
outer: while (++index < length) {
var value = array[index],
computed = iteratee == null ? value : iteratee(value);
value = comparator || value !== 0 ? value : 0;
if (isCommon && computed === computed) {
var valuesIndex = valuesLength;
while (valuesIndex--) {
if (values[valuesIndex] === computed) {
continue outer;
}
}
result.push(value);
} else if (!includes(values, computed, comparator)) {
result.push(value);
}
}
这是函数的核心遍历逻辑:
-
循环标签 :使用
outer:
标签,配合continue outer
实现从内层循环跳到外层循环的下一次迭代 -
值处理:
- 获取当前元素
value
- 如果提供了迭代器,计算
computed
值;否则使用原值 - 特殊处理
-0
和0
:当没有比较器且值为0
时,统一处理为0
- 获取当前元素
-
存在性检查(简单模式):
- 如果是简单模式且值不是
NaN
(computed === computed
用于检查非 NaN) - 手动遍历
values
数组查找匹配项 - 如果找到匹配项,跳过当前元素继续外循环
- 如果没有匹配项,将值添加到结果数组
- 如果是简单模式且值不是
-
存在性检查(高级模式):
- 如果不是简单模式或值是
NaN
- 使用之前选择的包含检查函数(
arrayIncludesWith
或cacheHas
) - 如果值不在
values
中,将其添加到结果数组
- 如果不是简单模式或值是
注意 computed === computed
这个巧妙的检查。在 JavaScript 中,只有 NaN
与自身不相等,所以这个表达式用于判断值是否为 NaN
。
6. 返回结果
js
return result;
最后返回包含差集元素的结果数组。
总结
baseDifference
是 Lodash 中实现差集操作的核心函数,它通过一系列的优化策略使得差集计算在各种场景下都能高效执行。其主要设计亮点包括:
- 灵活的接口设计:通过可选的迭代器和比较器参数,支持多种比较方式
- 智能的策略选择:根据数组大小和参数类型选择最优的查找策略
- 特殊值处理 :正确处理 JavaScript 中的边缘情况,如
-0
、0
和NaN
- 性能优化:使用循环标签、预缓存长度、SetCache 等技术提升性能
这个函数不仅展示了如何高效实现集合操作,还演示了处理 JavaScript 类型系统中各种边缘情况的最佳实践。从中我们可以学习到函数设计、性能优化和健壮性处理的重要技巧。