Lodash源码阅读-baseDifference

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 函数的核心思想是找出存在于第一个数组但不存在于第二个数组的所有元素。它通过以下几个步骤实现:

  1. 初始化阶段:设置默认参数和初始状态,创建结果数组
  2. 边界检查:对空数组进行快速处理
  3. 预处理输入:根据是否提供迭代器对值进行转换
  4. 优化检查策略:根据输入参数和数组大小选择最优的元素查找策略
  5. 遍历主数组:对每个元素判断是否存在于第二个数组中
  6. 构建结果:不存在于第二个数组的元素被添加到结果数组

函数通过智能地选择查找策略(直接遍历、使用比较器或使用 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):
    • 使用 cacheHasSetCache 提高查找效率
    • 设置 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);
  }
}

这是函数的核心遍历逻辑:

  1. 循环标签 :使用 outer: 标签,配合 continue outer 实现从内层循环跳到外层循环的下一次迭代

  2. 值处理

    • 获取当前元素 value
    • 如果提供了迭代器,计算 computed 值;否则使用原值
    • 特殊处理 -00:当没有比较器且值为 0 时,统一处理为 0
  3. 存在性检查(简单模式)

    • 如果是简单模式且值不是 NaNcomputed === computed 用于检查非 NaN)
    • 手动遍历 values 数组查找匹配项
    • 如果找到匹配项,跳过当前元素继续外循环
    • 如果没有匹配项,将值添加到结果数组
  4. 存在性检查(高级模式)

    • 如果不是简单模式或值是 NaN
    • 使用之前选择的包含检查函数(arrayIncludesWithcacheHas
    • 如果值不在 values 中,将其添加到结果数组

注意 computed === computed 这个巧妙的检查。在 JavaScript 中,只有 NaN 与自身不相等,所以这个表达式用于判断值是否为 NaN

6. 返回结果

js 复制代码
return result;

最后返回包含差集元素的结果数组。

总结

baseDifference 是 Lodash 中实现差集操作的核心函数,它通过一系列的优化策略使得差集计算在各种场景下都能高效执行。其主要设计亮点包括:

  1. 灵活的接口设计:通过可选的迭代器和比较器参数,支持多种比较方式
  2. 智能的策略选择:根据数组大小和参数类型选择最优的查找策略
  3. 特殊值处理 :正确处理 JavaScript 中的边缘情况,如 -00NaN
  4. 性能优化:使用循环标签、预缓存长度、SetCache 等技术提升性能

这个函数不仅展示了如何高效实现集合操作,还演示了处理 JavaScript 类型系统中各种边缘情况的最佳实践。从中我们可以学习到函数设计、性能优化和健壮性处理的重要技巧。

相关推荐
慧一居士22 分钟前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead24 分钟前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子6 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina6 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路7 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_7 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js