大家好,欢迎来到IT知识分享网。
观前须知
- 本文的目的绝非压榨代码性能,本文提供通俗易懂的方法,而不需要深度学习数据结构和算法。
- 具备 Map/Set 的知识储备会有所助益,因为本文的所有示例需要使用它们。
- 对于所有示例,我们都会测评 3 种不同方案: 原生 JS 数组方法(filter/reduce/map 等) Lodash 工具库 Map/Set
- 所有示例均包含性能基准测试,因为除非我们测评跑分,否则性能优化没有任何统计学意义。
- 在大多数跑分中,Lodash 比 Map/Set 有过之而无不及。但我仍会表演 Map/Set 的方案,毕竟我们可能不想安装 Lodash 依赖。
- 我只表演不可变操作,因为我的大部分工作都受益于不可变操作。
元素去重
原生数组方法
代码示例
list.filter((item, pos) => { return list.indexOf(item) === pos })
基准测试
|
时间(毫秒) |
数组元素 |
|
0.066284 |
10 |
|
0.004917 |
100 |
|
0. |
1_000 |
|
20.879 |
10_000 |
|
2028.42 |
100_000 |
|
. |
1_000_000 |
根据基准测试,此代码在处理 10_000 到 100_000 条记录时性能差强人意,超过该阈值则无法接受。
如果此代码在浏览器运行,那么在此期间我们的网站会卡死大约 3 分钟。
Lodash(原始值)
代码示例
_.uniq(list)
基准测试
|
时间(毫秒) |
数组元素 |
|
0.097815 |
10 |
|
0.074316 |
100 |
|
0.0 |
1_000 |
|
0. |
10_000 |
|
4.393 |
100_000 |
|
46.3184 |
1_000_000 |
夭寿啦!一旦高达 1_000_000 条记录,速度就会快近 4_000 倍!Lodash 绝对是正确的打开方式。
Lodash(非原始值)
上述优化性能惊人,但能且仅能用于原始值(字符串、数字、布尔值等)。
如果我们想基于属性实现元素唯一性,那该怎么办呢?
代码示例
_.uniqBy(list, comparator)
基准测试
|
时间(毫秒) |
数组元素 |
|
0. |
10 |
|
0.089563 |
100 |
|
0. |
1_000 |
|
5.5872 |
10_000 |
|
12.749 |
100_000 |
|
73.315 |
1_000_000 |
虽然性能降低了一点点,但测评跑分仍低于 100 毫秒!
Set(原始值)
代码示例
;[...new Set(list)]
Set 的方案简单粗暴,这能奏效,因为 Set 能且仅能接受唯一值。
基准测试
|
时间(毫秒) |
数组元素 |
|
0.0054639 |
10 |
|
0.0087427 |
100 |
|
0.05 |
1_000 |
|
0.055847 |
10_000 |
|
3.95355 |
100_000 |
|
43.326 |
1_000_000 |
测评跑分和 Lodash 平分秋色!现在我们可以把 Lodash 删了吧!
Map(原始值)
如果我们用非原始值测评上述例子,这无法奏效,因为 Set 能且仅能识别原始值的唯一性。此乃 Map 的用武之地!
代码示例
new Map(list.map(item => [extractKey(item), item])).values()
Map 的工作机制与 Set 类似,因为键值必须唯一,虽然但是,它们的键会映射到值!
基准测试
|
时间(毫秒) |
数组元素 |
|
0.0 |
10 |
|
0.0 |
100 |
|
0. |
1_000 |
|
1.03235 |
10_000 |
|
8.5266 |
100_000 |
|
158.005 |
1_000_000 |
测评跑分比 Lodash 慢了 2 倍。
虽然但是,如果我们想避免非必要的依赖,私以为这种性能也差强人意。
双列表比较
原生数组方法
代码示例
当我百度一下“JS 中的数组比较”时,StackOverflow 上爆料的首个答案是:
let difference = arr1.filter(x => !arr2.includes(x))
基准测试
|
时间(毫秒) |
数组元素 |
|
0.035791 |
1 |
|
0.0088525 |
10 |
|
0.015137 |
100 |
|
3.14404 |
1_000 |
|
313.424 |
10_000 |
|
31434. |
100_000 |
|
.0 |
1_000_000 |
测评跑分完全达咩。100_000 条记录一共需要 30 秒,速度慢如龟速。
但一旦达到 1_000_000,就耗时将近一小时。让我们瞄一下其他方案能否成功优化。
Lodash(原始值)
代码示例
_.difference(arr1, arr2)
基准测试
|
时间(毫秒) |
数组元素 |
|
0. |
1 |
|
0.066284 |
10 |
|
0. |
100 |
|
1.41284 |
1_000 |
|
11.8022 |
10_000 |
|
30.894 |
100_000 |
|
376.54 |
1_000_000 |
舒服了。即使有 1_000_000 条记录,我们连一秒钟都不需要!
Lodash(非原始值)
代码示例
举一反一,如果我们在非原始值的情况下测评跑分,它不再奏效。
幸运的是,Lodash 提供了解决方案。
_.differenceBy(arr1, arr2, comparator)
基准测试
|
时间(毫秒) |
数组元素 |
|
0. |
1 |
|
0.45929 |
10 |
|
1.6618 |
100 |
|
1.6905 |
1_000 |
|
15.1346 |
10_000 |
|
35.567 |
100_000 |
|
590.54 |
1_000_000 |
测评跑分慢了 200 毫秒,但性能仍对原生数组方法“降维打击”。
Set(原始值)
代码示例
const arr2Set = new Set(arr2) arr1.filter(x => !arr2Set.has(x))
基准测试
|
时间(毫秒) |
数组元素 |
|
0.046228 |
1 |
|
0.0057373 |
10 |
|
0.0 |
100 |
|
0. |
1_000 |
|
3.26617 |
10_000 |
|
43.272 |
100_000 |
|
737.47 |
1_000_000 |
举一反一,测评跑分比 Lodash 慢,但比原生数组方法快。
此方案比使用原生数组方法更快,是因为 Set.has 能奏效。Set 在存值时会计算其哈希值,并将该值存储在该键下。
这使得读写一个值需要 O(1) 时间复杂度,而 Array.includes 需要 O(n) 时间复杂度。
简直酷毙了,对不?
Map(非原始值)
代码示例
const arr2Set = new Map(arr2.map(x => [extractKey(x), x])) arr1.filter(x => !arr2Set.has(extractKey(x)))
基准测试
|
时间(毫秒) |
数组元素 |
|
0.096338 |
1 |
|
0.010327 |
10 |
|
0.01836 |
100 |
|
0.4906 |
1_000 |
|
4.3789 |
10_000 |
|
88.169 |
100_000 |
|
1597.02 |
1_000_000 |
这是第一个突破 1 秒标记的优化。
测评跑分仍比原生方法更快,但 2 倍的速度提升可能使得在项目中导入 Lodash 变得物有所值。
按属性合并列表
此操作采用 2 个具有共同属性的列表,并返回包含这些匹配对象的对象列表。
粉丝请注意:对于此操作,我们基于以下假设:
- 两个列表长度相同。
- 任一列表中都具有重复属性的元素。
- 每个列表中的每个元素在另一个列表中都有对应的元素。
原生 JS 方法(map/find)
代码示例
listB.map(b => ({ b: b, a: listA.find(a => a[aProperty] === b[bProperty]) }))
基准测试
|
时间(毫秒) |
数组元素 |
|
0.0 |
1 |
|
0.0 |
10 |
|
0. |
100 |
|
5.0087 |
1_000 |
|
208.89 |
10_000 |
|
20707. |
100_000 |
|
. |
1_000_000 |
梅开二度,使用 JS 数组方法又变慢了。
原生 JS 方法(reduce/map)
在为此操作的 Map/Set 版本进行基准测试时,我发现了另一种更高效的方案,来使用原生数组方法执行此操作。
代码示例
const listAMapById = listA.reduce((acc, a) => { return Object.assign(acc, { [a[aProperty]]: a }) }, {}) listB.map(b => ({ b: b, a: listAMapById[b[bProperty]] }))
在此示例中,我们将其中一个列表处理为一个对象,然后在查找另一个列表的对象时索引到该列表。
基准测试
|
时间(毫秒) |
数组元素 |
|
0.023169 |
1 |
|
0.0 |
10 |
|
0.082458 |
100 |
|
1.95825 |
1_000 |
|
7.2604 |
10_000 |
|
84.907 |
100_000 |
|
960.69 |
1_000_000 |
夭寿啦!使用原生 JS 数组方法,这一次并没有慢得令人窒息!
Lodash
代码示例
_.mergeWith(_.sortBy(listA, aProperty), _.sortBy(listB, bProperty), (a, b) => ({ a, b }))
我无法找到 Lodash 提供的开箱即用的方法,但我有一个大胆的想法。如果不满足上述任何假设,那么该方法也爱莫能助。
基准测试
|
时间(毫秒) |
数组元素 |
|
0.62683 |
1 |
|
0. |
10 |
|
0. |
100 |
|
2.10907 |
1_000 |
|
17.1033 |
10_000 |
|
194.81 |
100_000 |
|
6806.4 |
1_000_000 |
令人喵瞪狗呆的是,使用 Lodash 并不能吊打原生 JS 数组的性能!
这可能因为,在实际将两个数组合并之前,需要对它们排序造成的。
Map
代码示例
const listAMapByProperty = new Map(listA.map(a => [a[aProperty], a])) listB.map(b => ({ b, a: listAMapByProperty.get(b[bProperty]) }))
基准测试
|
时间(毫秒) |
数组元素 |
|
0.055847 |
1 |
|
0.0 |
10 |
|
0.0 |
100 |
|
0. |
1_000 |
|
2.85596 |
10_000 |
|
24.479 |
100_000 |
|
576.91 |
1_000_000 |
这次原生方法可能击败了 Lodash,但在此情况下,使用 Map 似乎是其中最快的。
高能总结
这些是我在这些基准测试中收获的东东。
使用 Lodash 是最快的(大多数情况下)
运行这些基准测试后,我阅读了我使用的 Lodash 方法的源码。
大多数情况下,Lodash 使用 Map 和 Set 来获得这种性能。
虽然但是,Lodash 也进行了为微调,挤出了额外的性能优势。
因此,如果性能对您而言兹事体大,且您不介意导入 npm 包,那么如果您正在处理包含海量元素的数据,您可以优先使用 Lodash。
然而情况并非总是如此,因此粉丝请务必深度学习多种方案,运行基准测试。
您不需要 Lodash 来获得优秀的性能
虽然 Lodash 是最快的,但如果没有 Lodash,我们也有其他无限逼近其速度的技术方案。
Map/Set 都棒棒哒!
运行所有基准测试后,我肯定会开始在代码中更多地使用 Set/Map。
它们不仅速度惊人,而且有手就行,并提供了良好的 API 来操作。
JS 数组方法对于少量数据而言足够快。
如果运行的数组的元素数量不超过 10_000,那可能不需要过早的性能优化。
我进行基准测试的所有操作,在该体量的数据集上执行的时间都超过 300 毫秒。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/97352.html