Lodash源码阅读-equalArrays

Lodash 源码阅读-equalArrays

功能概述

equalArrays 是 Lodash 库中的一个内部工具函数,专门用于深度比较两个数组是否相等。它是 baseIsEqualDeep 函数的数组专用版本,支持部分深度比较和无序比较,能够处理循环引用等复杂情况。这个函数是 Lodash 中 _.isEqual 方法的核心组件之一,为 Lodash 提供了强大而灵活的深度相等性检查能力。

前置学习

依赖函数

  • arraySome:用于检查数组中是否有元素满足条件
  • cacheHas:检查缓存中是否存在特定键
  • SetCache:一个用于存储唯一值的缓存结构
  • Stack:用于跟踪遍历过的对象,处理循环引用

技术知识

  • 位掩码(Bitmask) :使用二进制位表示多个布尔标志
    • COMPARE_PARTIAL_FLAG(值为 1):表示进行部分比较
    • COMPARE_UNORDERED_FLAG(值为 2):表示进行无序比较
  • 循环引用检测:处理对象间的循环引用问题
  • 深度比较:递归比较嵌套数据结构
  • 自定义比较器:支持用户自定义的比较逻辑

源码实现

javascript 复制代码
/**
 * A specialized version of `baseIsEqualDeep` for arrays with support for
 * partial deep comparisons.
 *
 * @private
 * @param {Array} array The array to compare.
 * @param {Array} other The other array to compare.
 * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
 * @param {Function} customizer The function to customize comparisons.
 * @param {Function} equalFunc The function to determine equivalents of values.
 * @param {Object} stack Tracks traversed `array` and `other` objects.
 * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
 */
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
  var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
    arrLength = array.length,
    othLength = other.length;

  if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
    return false;
  }
  // Check that cyclic values are equal.
  var arrStacked = stack.get(array);
  var othStacked = stack.get(other);
  if (arrStacked && othStacked) {
    return arrStacked == other && othStacked == array;
  }
  var index = -1,
    result = true,
    seen = bitmask & COMPARE_UNORDERED_FLAG ? new SetCache() : undefined;

  stack.set(array, other);
  stack.set(other, array);

  // Ignore non-index properties.
  while (++index < arrLength) {
    var arrValue = array[index],
      othValue = other[index];

    if (customizer) {
      var compared = isPartial
        ? customizer(othValue, arrValue, index, other, array, stack)
        : customizer(arrValue, othValue, index, array, other, stack);
    }
    if (compared !== undefined) {
      if (compared) {
        continue;
      }
      result = false;
      break;
    }
    // Recursively compare arrays (susceptible to call stack limits).
    if (seen) {
      if (
        !arraySome(other, function (othValue, othIndex) {
          if (
            !cacheHas(seen, othIndex) &&
            (arrValue === othValue ||
              equalFunc(arrValue, othValue, bitmask, customizer, stack))
          ) {
            return seen.push(othIndex);
          }
        })
      ) {
        result = false;
        break;
      }
    } else if (
      !(
        arrValue === othValue ||
        equalFunc(arrValue, othValue, bitmask, customizer, stack)
      )
    ) {
      result = false;
      break;
    }
  }
  stack["delete"](array);
  stack["delete"](other);
  return result;
}

实现思路

equalArrays 函数的实现思路可以分为以下几个关键步骤:

  1. 长度检查 :首先比较两个数组的长度,如果长度不同且不是部分比较模式(或部分比较模式下第二个数组不长于第一个数组),则直接返回 false

  2. 循环引用检查 :使用 stack 对象检查是否存在循环引用,如果两个数组已经在比较栈中,则直接比较它们的引用关系。

  3. 设置比较标记:将两个数组相互添加到比较栈中,标记它们正在被比较,以处理可能的循环引用。

  4. 元素比较

    • 如果提供了自定义比较器 customizer,则使用它进行比较
    • 如果是无序比较模式,使用 SetCachearraySome 检查每个元素是否在另一个数组中有匹配项
    • 如果是有序比较模式,直接按索引比较对应元素
  5. 清理和返回:从比较栈中移除两个数组,并返回比较结果。

这种实现方式既灵活又强大,能够处理各种复杂的数组比较场景,包括嵌套数组、循环引用和自定义比较逻辑。

源码解析

参数解析

javascript 复制代码
function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {

函数接收六个参数:

  • array:要比较的第一个数组
  • other:要比较的第二个数组
  • bitmask:位掩码标志,控制比较的行为
  • customizer:自定义比较函数,允许用户定制比较逻辑
  • equalFunc:用于确定值是否相等的函数,通常是 baseIsEqual
  • stack:跟踪已遍历对象的栈,用于处理循环引用

位掩码处理

javascript 复制代码
var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
  arrLength = array.length,
  othLength = other.length;

这里使用位运算从 bitmask 中提取标志:

  • isPartial:如果设置了 COMPARE_PARTIAL_FLAG(值为 1),则为 true,表示进行部分比较
  • 同时获取两个数组的长度,用于后续比较

长度检查

javascript 复制代码
if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
  return false;
}

这个条件检查两种情况:

  1. 如果两个数组长度不同,且不是部分比较模式,则返回 false
  2. 如果是部分比较模式,但第二个数组长度小于第一个数组,也返回 false

这意味着在部分比较模式下,第二个数组可以比第一个数组长,但反之则不行。

循环引用检查

javascript 复制代码
var arrStacked = stack.get(array);
var othStacked = stack.get(other);
if (arrStacked && othStacked) {
  return arrStacked == other && othStacked == array;
}

这段代码检查两个数组是否已经在比较栈中(即它们是否已经被比较过):

  • 如果两个数组都已经在栈中,则检查它们是否互相引用对方
  • 这是处理循环引用的关键部分,防止无限递归

循环引用详解

循环引用是指数据结构中的元素引用了自身或其祖先元素,形成一个闭环。在 JavaScript 中,这种情况会导致常规的深度比较算法陷入无限递归,最终导致栈溢出错误。equalArrays函数通过巧妙的设计解决了这个问题。

循环引用检测机制

循环引用检测的核心思想是使用一个Stack数据结构来跟踪当前正在比较的对象对。stack对象维护了两个重要的映射:

  1. array -> other: 记录第一个数组映射到第二个数组
  2. other -> array: 记录第二个数组映射到第一个数组

当函数递归比较数组元素时,如果遇到已经在比较过程中的数组,就会触发循环引用检查。

关键的判断条件是:

javascript 复制代码
arrStacked == other && othStacked == array;

这个条件检查:

  • 当前的other是否就是之前为array记录的值
  • 当前的array是否就是之前为other记录的值

只有两个条件同时满足,才能确认这是一个结构相同的循环引用模式。

具体例子
例子 1:数组直接自引用

考虑两个简单的自引用数组:

javascript 复制代码
var arr1 = [];
arr1.push(arr1); // arr1 = [arr1]

var arr2 = [];
arr2.push(arr2); // arr2 = [arr2]

_.isEqual(arr1, arr2); // 返回 true

equalArrays比较这两个数组时:

  1. 初始阶段

    • arr1arr2添加到栈中:stack.set(arr1, arr2)stack.set(arr2, arr1)
  2. 元素比较

    • 比较arr1[0]arr2[0]
    • 因为arr1[0] === arr1arr2[0] === arr2,所以会递归调用equalFunc
  3. 循环引用检测

    • 当再次比较arr1arr2时,发现它们已经在栈中
    • 获取arrStacked = stack.get(arr1) = arr2
    • 获取othStacked = stack.get(arr2) = arr1
    • 检查arr2 === arr2[0]arr1 === arr1[0],两者都为true
    • 确认这是结构相同的循环引用,返回true
例子 2:嵌套数组循环引用

复杂一点的例子:

javascript 复制代码
var arr1 = [1, 2];
var subarr1 = [3, arr1];
arr1.push(subarr1); // arr1 = [1, 2, [3, arr1]]

var arr2 = [1, 2];
var subarr2 = [3, arr2];
arr2.push(subarr2); // arr2 = [1, 2, [3, arr2]]

_.isEqual(arr1, arr2); // 返回 true

比较过程:

  1. 比较头两个元素(1 和 2):完全相等

  2. 比较第三个元素(子数组):

    • 比较subarr1[0]subarr2[0]:两者都是 3,相等
    • 比较subarr1[1]subarr2[1]:递归调用equalFunc比较arr1arr2
  3. 递归比较时:

    • 发现arr1arr2已经在栈中
    • 检查stack.get(arr1) === arr2stack.get(arr2) === arr1
    • 两者都为true,确认循环引用结构相同,返回true
例子 3:不同结构的循环引用
javascript 复制代码
var arr1 = [];
arr1.push(arr1); // arr1 = [arr1]

var arr2 = [[]];
arr2[0].push(arr2); // arr2 = [[arr2]]

_.isEqual(arr1, arr2); // 返回 false

比较过程:

  1. 比较arr1[0]arr2[0]
    • arr1[0] === arr1,但arr2[0]是一个独立的数组,不等于arr2
    • 需要递归比较arr1arr2[0]
  2. 在递归比较过程中:
    • 虽然两者都有循环引用,但结构不同
    • 无法满足循环引用检测条件
    • 最终返回false
为什么设计成双向检查?

双向检查(arrStacked == other && othStacked == array)是为了确保循环引用的结构完全一致。单向检查可能会误判一些情况,例如:

javascript 复制代码
var a = [];
var b = [a];
a.push(b); // a引用b,b引用a

var c = [];
var d = [c];
c.push(d); // c引用d,d引用c

尽管ac都参与了循环引用,但它们的结构实际上是一致的。双向检查能够识别出这种对称性,确保结构相同的循环引用被正确地判定为相等。

Stack 的作用

Stack数据结构不仅用于循环引用检测,还承担了几个重要作用:

  1. 防止无限递归:避免陷入无限深的比较循环
  2. 识别等价结构:确定不同引用但结构相同的循环引用
  3. 提高性能:已比较过的对象对可以直接返回结果,无需重复比较

这种设计使得 Lodash 能够安全、高效地比较包含循环引用的复杂数据结构,是深度相等比较功能的关键部分。

初始化比较状态

javascript 复制代码
var index = -1,
  result = true,
  seen = bitmask & COMPARE_UNORDERED_FLAG ? new SetCache() : undefined;

stack.set(array, other);
stack.set(other, array);

这里初始化了比较所需的变量:

  • index:当前比较的索引,初始为 -1
  • result:比较结果,初始为 true
  • seen:如果是无序比较模式(设置了 COMPARE_UNORDERED_FLAG,值为 2),则创建一个 SetCache 实例用于跟踪已比较的元素

同时,将两个数组互相添加到比较栈中,标记它们正在被比较。

元素比较循环

javascript 复制代码
while (++index < arrLength) {
  var arrValue = array[index],
      othValue = other[index];

这个循环遍历数组的每个元素,逐一进行比较。

自定义比较器处理

javascript 复制代码
if (customizer) {
  var compared = isPartial
    ? customizer(othValue, arrValue, index, other, array, stack)
    : customizer(arrValue, othValue, index, array, other, stack);
}
if (compared !== undefined) {
  if (compared) {
    continue;
  }
  result = false;
  break;
}

如果提供了自定义比较器:

  • 根据是否是部分比较模式,调用比较器时参数顺序会有所不同
  • 如果比较器返回了明确的结果(不是 undefined):
    • 如果结果为真值,继续比较下一个元素
    • 如果结果为假值,设置 resultfalse 并跳出循环
为什么要改变参数顺序?

参数顺序的改变反映了比较的方向或重点:

  1. 完整比较模式isPartial = false):

    • 我们将array作为主要对象,other作为比较对象
    • 判断的是两个数组是否完全相等(双向比较)
    • 因此参数顺序是customizer(arrValue, othValue, ...)
  2. 部分比较模式isPartial = true):

    • 我们将other作为主要对象,array作为要检查的对象
    • 判断的是array是否是other的子集(单向比较)
    • 因此参数顺序是customizer(othValue, arrValue, ...)

这种设计使得自定义比较器在不同的比较模式下能保持一致的语义,即第一个参数总是"主要对象"的元素,第二个参数总是"比较对象"的元素。

无序比较模式

javascript 复制代码
if (seen) {
  if (!arraySome(other, function(othValue, othIndex) {
        if (!cacheHas(seen, othIndex) &&
            (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
          return seen.push(othIndex);
        }
      })) {
    result = false;
    break;
  }
}

如果是无序比较模式(seen 存在):

  • 使用 arraySome 在第二个数组中查找与当前元素相等的元素
  • 如果找到匹配项,将其索引添加到 seen 缓存中,表示该元素已被匹配
  • 如果没有找到匹配项,设置 resultfalse 并跳出循环

这允许数组元素的顺序不同,只要所有元素都能找到对应的匹配项即可。

有序比较模式

javascript 复制代码
else if (!(
      arrValue === othValue ||
        equalFunc(arrValue, othValue, bitmask, customizer, stack)
    )) {
  result = false;
  break;
}

如果是有序比较模式(seen 不存在):

  • 直接比较对应索引位置的元素是否相等
  • 首先尝试使用严格相等(===)进行比较
  • 如果不相等,则使用 equalFunc(通常是 baseIsEqual)进行深度比较
  • 如果两种比较都失败,设置 resultfalse 并跳出循环

清理和返回结果

javascript 复制代码
stack["delete"](array);
stack["delete"](other);
return result;

最后,从比较栈中移除两个数组,并返回比较结果。这一步很重要,它确保了栈的正确状态,防止内存泄漏和错误的循环引用检测。

总结

equalArrays 是 Lodash 中一个强大而复杂的内部工具函数,专门用于深度比较数组是否相等。它支持多种比较模式,能够处理循环引用、嵌套结构和自定义比较逻辑,是 _.isEqual 方法的核心组件之一。

这个函数的实现体现了几个重要的编程原则和技术:

  1. 位掩码技术:使用二进制位表示多个布尔标志,提高参数传递的效率
  2. 循环引用处理:使用栈跟踪已比较的对象,防止无限递归
  3. 短路求值:一旦发现不相等的元素,立即返回结果,避免不必要的比较
  4. 灵活的比较策略:支持有序比较、无序比较和部分比较等多种模式
  5. 可扩展性:通过自定义比较器支持用户定制的比较逻辑

理解 equalArrays 的工作原理,有助于我们更好地使用 Lodash 的 _.isEqual 方法,并在自己的代码中实现更高效、更可靠的深度比较逻辑。

相关推荐
Qrun3 小时前
Windows11安装nvm管理node多版本
前端·vscode·react.js·ajax·npm·html5
中国lanwp3 小时前
全局 npm config 与多环境配置
前端·npm·node.js
JELEE.4 小时前
Django登录注册完整代码(图片、邮箱验证、加密)
前端·javascript·后端·python·django·bootstrap·jquery
TeleostNaCl6 小时前
解决 Chrome 无法访问网页但无痕模式下可以访问该网页 的问题
前端·网络·chrome·windows·经验分享
前端大卫7 小时前
为什么 React 中的 key 不能用索引?
前端
你的人类朋友7 小时前
【Node】手动归还主线程控制权:解决 Node.js 阻塞的一个思路
前端·后端·node.js
小李小李不讲道理9 小时前
「Ant Design 组件库探索」五:Tabs组件
前端·react.js·ant design
毕设十刻9 小时前
基于Vue的学分预警系统98k51(程序 + 源码 + 数据库 + 调试部署 + 开发环境配置),配套论文文档字数达万字以上,文末可获取,系统界面展示置于文末
前端·数据库·vue.js
mapbar_front10 小时前
在职场生存中如何做个不好惹的人
前端
牧杉-惊蛰10 小时前
纯flex布局来写瀑布流
前端·javascript·css