Microdiff: 构建最快的对象和数组差异化

原文信息: 查看原文查看原文

Building the fastest object and array differ

- Jacob Jackson

我维护着 Microdiff,这是一个针对深层对象差异化进行了性能和尺寸优化的库。有人在一个 Microdiff 的问题 中发表了一个帖子,要求我写一篇关于我如何使 Microdiff 变得快速的博客文章。

我相信,对于你注意到的其他库存在的效率低下以及你是如何克服它们的(包括你说的不支持的“边缘情况”)的解释(也许是一篇博客文章?)将会很有趣。它们也将有助于正确解释基准测试结果。

因此,我决定这样做。这篇博客文章描述了我如何使 Microdiff 比大多数其他对象和数组差异化库更快。

免责声明:我在这个问题上有很强的偏见。

差异化介绍

差异化(difference tracking)是追踪两个对象之间的不同之处。例如,假设你有两个对象,对象 a 和对象 b。

const a = {
  bananas: true,
  apples: true,
  peaches: true,
};
const b = {
  bananas: true,
  apples: false,
  lemons: true,
};

使用 Microdiff,要获取这些差异,你会这样做:

import diff from "microdiff";
console.log(JSON.stringify(microdiff(a, b)));

/*
[
  {
    'type':'CHANGE',
    'path':['apples'],
    'value':false,
    'oldValue':true},
  {
    'type':'REMOVE',
    'path':['peaches'],
    'oldValue':true
  },
  {
    'type':'CREATE',
    'path':['lemons'],
    'value':true
  }
]
*/

如你所见,所有的更改,无论是值的更改、添加还是移除,都被记录下来。差异化对于许多事情都是必不可少的,比如虚拟 DOM,因为它们需要记录元素的变化。现在,让我们了解一下 Microdiff 之前的差异化生态系统存在的问题。

Microdiff 之前的差异化生态系统

差异化生态系统处于糟糕的状态。许多库虽然下载量很大,但并没有得到积极的维护,并且制作质量很差。现在,让我们来看看我们的第一个例子,deep-diff。

Deep-Diff 是最受欢迎的 JavaScript 库之一,用于深层对象差异化。它每周的下载量在 100 万到 200 万之间,并且被拥有超过 10k GitHub 星的工具使用。然而,它也有许多缺陷。首先,最后一次提交是在 2019 年,它不遵循现代约定,比如不支持 ESM 并且不提供捆绑的 TypeScript 类型。

此外,它的尺寸和性能也存在问题。它的大小为 5.5kb 压缩后和 1.9kb Gzipped。尺寸并不是很糟糕,但考虑到这只是一个简单的实用程序,因此应该有更小的尺寸。相比之下,Microdiff 的大小为 0.9kb 压缩后和 0.5kb Gzipped。现在,就性能而言,Deep-Diff 也表现不佳。它并不是为了尺寸小和速度快而设计的,因为它有许多不同的功能,这增加了很大的开销。此外,它不会像分组类型行为一样去做一些优化以提高性能。由于这些原因,Microdiff 可以比 快 400%

Deep-Object-Diff 是另一个流行的差异化库。虽然它自 2018 年以来没有更新过,但它拥有一些 Deep-Diff 缺失的现代特性,比如 ESM 和内置的 TypeScript 类型。此外,如果你使用基本的差异化,它的性能可以接近 Microdiff。然而,它仍然有两个问题,尺寸和提供的信息。首先,虽然它没有 deep-diff 那么大,但它仍然很大,压缩后大小为 5.2kb,Gzipped 大小为 1kb。其次,由于输出设计的方式,它提供的细节很少。Microdiff 提供了更多的细节,比如变更类型、新值、旧值和路径,而 Deep-Object-Diff 的最详细的差异化(detailedDiff)并不提供旧值。另外,如果你想要接近 Microdiff 的速度,你必须使用主要的差异化函数而不是 detailedDiff,这样你就不知道变更类型了。

虽然 JSDiff 支持对象差异化,但它主要设计用于文本差异化。它很大,压缩后大小为 15.8kb,Gzipped 大小为 5.9kb,并且非常慢(比 Microdiff 慢 2100%)。我不会深入讨论它为什么如此慢,因为它简单来说并不是为对象差异化而设计的。

Microdiff 如何解决这个问题

专注于性能的架构

Microdiff 通过专注于性能和尺寸而解决了许多这些问题,而不会牺牲易用性。它不是拥有一堆复杂的函数,而是一个简单的递归函数。Microdiff 还使用了诸如组合类型行为等策略,以减小尺寸同时提高性能。例如,假设你想要查看正则表达式和 JavaScript 日期之间的差异。为了准确追踪变化,你必须将正则表达式转换为字符串,并将日期转换为数字。这个问题的一个朴素的解决方案可能是这样的:

if (value instanceof RegExp && value2 instanceof RegExp) {
  return value.toString() === value.toString();
} else if (value instanceof Date && value2 instanceof Date) {
  return Number(value) === Number(value2);
}

这样可以解决问题,但是如果你还需要检查 new String() 对象或 new Number() 对象呢?(new String()new Number() 不创建原始值,所以你必须像处理日期和正则表达式那样将它们转换为原始值)为了解决这个问题,Microdiff 的实现更像是这样的:

const richTypes = { Date: true, RegExp: true, String: true, Number: true };
if (richTypes[Object.getPrototypeOf(value).constructor.name]) {
  return isNaN(value)
    ? value.toString() === value2.toString()
    : Number(value) === Number(value2);
}

这段代码首先获取一个不能直接比较的类型列表(richTypes)。然后,它检查值是否属于这些类型之一。如果是,那么代码检查值是否可以通过 isNaN 转换为数字。如果可以(在日期和 new Number() 的情况下是 true),它将检查转换为数字的版本。如果不能(在正则表达式和 new String() 的情况下是 true),它将强制将值转换为字符串并比较该版本。在 Microdiff 中,实际的丰富类型转换逻辑并不完全相同,尽管存在一些差异,这些差异会减小尺寸并有助于使逻辑与代码的其余部分匹配。

这些原因是 Microdiff 之所以快速的部分。但另一个原因是它只关注更常见的情况,而不是每一个可能的边缘情况。

关注 99% 的情况而不是解决所有边缘情况

在这方面,自从发布以来,Microdiff 已经取得了巨大的进步。事实上,自从撰写了 最初的解释 以来,Microdiff 已经增加了对更多丰富类型和循环引用的支持。然而,仍然存在一些情况,Microdiff 的行为不够正确,比如当比较具有原型属性的对象时,因为它包括了原型属性。类型组合解决了列出的类型的这个问题,但对于所有其他类型并不适用。在以前的测试中,排除原型属性的方法并不快。但是,我可能会添加一种方式,让你可以传递自定义的继承类型来进行字符串/数字转换,这可能有助于某些情况。然而,目前还不可能实现这一点。

结论

总之,Microdiff 是最快的差异化库,因为它专注于性能的架构和关注 99% 的情况,而且 Microdiff 仍然能够使用现代特性并且易于使用。如果你对 Microdiff 感兴趣,请查看 GitHub 仓库。我希望你从中学到了一些东西,感谢你的阅读。

分享于 2024-03-25

访问量 29

预览图片