- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
useArrayDifference是 VueUse 中用于响应式求两个数组差集的组合式函数,它把集合论中的**相对补集(A \ B)与对称差集(A △ B)**运算带进了 Vue 3 的响应式世界:只要传入的数组是 ref、reactive 数组或 getter 函数,结果就会自动跟随数据变化而更新。本文将以官方文档 packages/shared/useArrayDifference/index.md 为骨架,结合其源码实现 index.ts 与测试用例 index.test.ts,完整讲解它的三种调用形态、symmetric选项的底层机制,以及它在 VueUse 数组工具家族中的定位,读完后你可以在自己的项目中直接落地使用。
一、函数签名与核心概念
useArrayDifference是一个惰性计算的组合式函数:它本身不执行差集运算,而是返回一个ComputedRef<T[]>,只有当你访问.value时才会真正计算结果,并在依赖的数组变化时自动重新计算。
参数总览
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
list | MaybeRefOrGetter<T[]> | 是 | 主数组(集合 A),可以传 ref、普通数组或返回数组的 getter 函数 |
values | MaybeRefOrGetter<T[]> | 是 | 被比较的数组(集合 B),同样支持 ref / 普通数组 / getter |
key(形态一) | keyof T | 否 | 指定按对象哪个属性比较,等价于compareFn = (a, b) => a[key] === b[key] |
compareFn(形态二) | (value: T, othVal: T) => boolean | 否 | 自定义比较函数,返回true表示两个元素"相等" |
options | UseArrayDifferenceOptions | 否 | 配置对象,目前仅含symmetric选项 |
UseArrayDifferenceOptions的定义在 index.ts 中:
export interface UseArrayDifferenceOptions { /** * Returns asymmetric difference * @see https://en.wikipedia.org/wiki/Symmetric_difference * @default false */ symmetric?: boolean }返回值类型为UseArrayDifferenceReturn<T> = ComputedRef<T[]>(见 index.ts)。
两种差集语义
- 相对补集(Asymmetric Difference):默认行为,即
A \ B,只包含属于 A 但不属于 B的元素,数学上称为 "B 在 A 中的相对补集"。 - 对称差集(Symmetric Difference):传入
{ symmetric: true }后,得到A △ B,即只属于 A 或只属于 B 的元素并集,等价于(A \ B) ∪ (B \ A)。
这两者也是官方文档开篇就点明的核心语义(index.md)。
二、基础用法:与响应式数组配合
最直接的使用方式是传入两个 ref 数组,官方文档给出了一个完整的响应式示例(index.md):
import { useArrayDifference } from '@vueuse/core' const list1 = ref([0, 1, 2, 3, 4, 5]) const list2 = ref([4, 5, 6]) const result = useArrayDifference(list1, list2) // result.value: [0, 1, 2, 3] list2.value = [0, 1, 2] // result.value: [3, 4, 5]这里有两个值得注意的细节:
- 结果是响应式的:
result.value在list2被整体重新赋值后自动从[0, 1, 2, 3]变为[3, 4, 5],这正是computed的特性——useArrayDifference内部使用toValue()读取 ref 的最新值,并把这些读取包装在computed中建立依赖追踪。 - 保持原数组顺序与元素形态:结果来自
Array.prototype.filter,因此输出顺序与list1一致,且保留的是list1中的原引用,而不是拷贝。
测试用例 index.test.ts 验证了这一行为,包括把list1也改掉后结果变为空数组的场景:
const list1 = deepRef([1, 2, 3, 4, 5]) const list2 = deepRef([4, 5, 6]) const result = useArrayDifference(list1, list2) expect(result.value).toEqual([1, 2, 3]) list2.value = [1, 2, 3] expect(result.value).toEqual([4, 5]) list1.value = [1, 2, 3] expect(result.value).toEqual([])三、自定义比较函数:处理对象数组
当数组元素是对象时,默认的严格相等比较(===)无法正确工作,此时需要传入第三个参数——自定义比较函数。官方文档示例(index.md):
import { useArrayDifference } from '@vueuse/core' const list1 = ref([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]) const list2 = ref([{ id: 4 }, { id: 5 }, { id: 6 }]) const result = useArrayDifference(list1, list2, (value, othVal) => value.id === othVal.id) // result.value: [{ id: 1 }, { id: 2 }, { id: 3 }]比较函数compareFn(value, othVal)的语义:返回true表示"两个元素视为相等"。从源码看(index.ts),计算逻辑是:
const diff1 = computed(() => toValue(list).filter(x => toValue(values).findIndex(y => compareFn(x, y)) === -1, ), )也就是说,list1中每个元素x,只要在list2里找不到任何一个与之相等的元素(findIndex返回-1),就保留在差集结果中。
如果不传比较函数,默认使用严格相等比较(index.ts):
function defaultComparator<T>(value: T, othVal: T) { return value === othVal }因此对于基础类型(数字、字符串)数组,直接省略第三参数即可;只有对象、NaN等无法通过===精确比较的场景才需要自定义compareFn。
四、按键名比较:更简洁的字符串重载
对于"按对象某个属性比较"这一高频场景,useArrayDifference还提供了更简洁的重载:第三参数直接传属性名字符串。对应的类型签名在 index.ts:
export function useArrayDifference<T>( list: MaybeRefOrGetter<T[]>, values: MaybeRefOrGetter<T[]>, key?: keyof T, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn<T> export function useArrayDifference<T>( list: MaybeRefOrGetter<T[]>, values: MaybeRefOrGetter<T[]>, compareFn?: (value: T, othVal: T) => boolean, options?: UseArrayDifferenceOptions, ): UseArrayDifferenceReturn<T>当第三参数是字符串时,实现会把key展开为一个比较函数(index.ts):
if (typeof compareFn === 'string') { const key = compareFn as keyof T compareFn = (value: T, othVal: T) => value[key] === othVal[key] }用法示例:
const list1 = ref([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]) const list2 = ref([{ id: 3 }, { id: 4 }, { id: 5 }]) const result = useArrayDifference(list1, list2, 'id') // result.value: [{ id: 1 }, { id: 2 }]这与显式写(value, othVal) => value.id === othVal.id完全等价。测试用例 index.test.ts 覆盖了该重载,并验证了数组变更后结果同步更新的行为。
五、对称差集:开启symmetric选项
如果业务需要的是"只属于 A 或只属于 B 的元素"(例如同步两份数据时找出两侧各自独有的记录),应使用对称差集。官方文档示例(index.md):
import { useArrayDifference } from '@vueuse/core' const list1 = ref([{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }]) const list2 = ref([{ id: 4 }, { id: 5 }, { id: 6 }]) const result = useArrayDifference( list1, list2, (value, othVal) => value.id === othVal.id, { symmetric: true }, ) // result.value: [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 6 }]注意结果顺序:先输出A \ B([{ id: 1 }, { id: 2 }, { id: 3 }]),再拼接B \ A([{ id: 6 }]),最终[{ id: 1 }, { id: 2 }, { id: 3 }, { id: 6 }]。
从源码看对称差集的实现(index.ts),它实际计算了两个方向的差集再合并:
if (symmetric) { const diff2 = computed(() => toValue(values).filter(x => toValue(list).findIndex(y => compareFn(x, y)) === -1, ), ) return computed(() => symmetric ? [...toValue(diff1), ...toValue(diff2)] : toValue(diff1)) } else { return diff1 }两个方向都保持各自原数组的顺序:diff1来自list1的过滤结果,diff2来自list2的过滤结果。测试用例 index.test.ts 对symmetric与 key 形态、compareFn 形态的组合均做了验证,包括两个数组完全一致时结果为空、两个数组互不相交时结果为两者并集等边界场景。
六、响应式原理剖析:toValue+computed的组合
理解了用法之后,值得深入看一下它的响应式内核。整个函数的实现非常精简(index.ts),关键点有三个:
1. 统一解包MaybeRefOrGetter
参数类型MaybeRefOrGetter<T[]>意味着你既可以传ref([1, 2, 3]),也可以传普通数组[1, 2, 3],甚至传一个返回数组的函数:
const result = useArrayDifference(() => [1, 2, 3], list2)实现内部统一通过 Vue 的toValue()解包(index.ts),这也是 Vue 3.3+ 组合式 API 的标准做法。由于toValue是在computed的 getter 内调用的,任何 ref 依赖都会被自动追踪——这就是"响应式"的来源。
2. 惰性计算与依赖追踪
函数返回的是ComputedRef,差集运算只有在读取.value时才执行。只要list1或list2指向的 ref 发生变更,下一次读取时就会基于最新值重新计算。函数声明上的@__NO_SIDE_EFFECTS__注解(index.ts)也表明它不会产生任何副作用,可以被安全地摇树(tree-shaking)与任意调用。
3. 相等判定的语义边界
由于使用findIndex查找"是否存在相等元素",useArrayDifference处理的是元素级去重视角下的差集,而不是下标对齐的"位置差"。例如list1 = [1, 2, 3]、list2 = [3, 4, 5],结果为[1, 2];即使元素在数组中的位置不同,只要"相等"(由compareFn决定)就被排除。这也是它与filter、includes等原生 API 组合行为的天然差异,使用时需留意重复元素场景(如list1中有两个1,而list2中有一个1,则结果中仍保留一个1,因为findIndex只判断"是否存在")。
七、在 VueUse 数组工具家族中的位置
useArrayDifference属于 VueUse 的 shared 包,并被统一从 packages/shared/index.ts 导出(export * from './useArrayDifference'),最终通过@vueuse/core提供给所有使用者,因此文档中的导入语句import { useArrayDifference } from '@vueuse/core'是标准写法。
它与 shared 包中的其他数组工具共同构成一套完整的"响应式数组操作"能力,这些函数全部接受MaybeRefOrGetter<T[]>并返回响应式结果(全部列于 packages/shared/index.ts):
| 组合式函数 | 对应原生能力 | 说明 |
|---|---|---|
useArrayDifference | 集合差集 | 本文主角,A \ B 或 A △ B |
useArrayEvery | Array.every | 是否所有元素满足条件 |
useArrayFilter | Array.filter | 响应式过滤 |
useArrayFind/useArrayFindIndex/useArrayFindLast | Array.find系列 | 响应式查找元素 / 下标 / 最后一个匹配 |
useArrayIncludes | Array.includes | 响应式包含判断(index.md) |
useArrayJoin | Array.join | 响应式拼接 |
useArrayMap | Array.map | 响应式映射 |
useArrayReduce | Array.reduce | 响应式归约 |
useArraySome | Array.some | 是否存在满足条件的元素 |
useArrayUnique | 去重 | 响应式数组去重 |
在数据对比、表单校验、标签同步、购物车合并去重等场景中,useArrayDifference往往与useArrayUnique、useArrayIncludes搭配使用,可以完全摆脱手写watch+ 手动计算的状态同步代码。
八、安装与快速上手
useArrayDifference随@vueuse/core发布,安装后即可直接使用:
npm i @vueuse/core # 或 pnpm add @vueuse/core / yarn add @vueuse/core然后按需引入即可,无需额外注册:
import { useArrayDifference } from '@vueuse/core' const result = useArrayDifference( ref([{ id: 1 }, { id: 2 }]), ref([{ id: 2 }, { id: 3 }]), 'id', { symmetric: true }, ) // result.value: [{ id: 1 }, { id: 3 }]由于导出的是纯 ESM 组合式函数且带@__NO_SIDE_EFFECTS__标记,主流打包器都能对它做 tree-shaking,只把用到的函数打进产物。若你想亲自验证本文所有行为,可以直接查看并运行仓库中的测试文件 index.test.ts,它用 Vitest 完整覆盖了基础差集、compareFn、key 形态以及两种 symmetric 组合共 5 组断言场景。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
Ryujinx 模拟器使用指南:如何构建、启动 Switch 模拟器并解决常见报错
Ryujinx 模拟器使用指南:如何构建、启动 Switch 模拟器并解决常见报错 Ryujinx 是一款用 C 编写的开源 Nintendo Switch 模
硬件仿真图形学es-toolkit 的 xorWith:用自定义比较函数计算数组对称差集的完整指南
es toolkit 的 xorWith:用自定义比较函数计算数组对称差集的完整指南 xorWith 是 es toolkit 数组模块中用于计算 对称差集(s
前端后端rui多平台开发指南:如何用同一套代码部署到桌面和移动端
rui多平台开发指南:如何用同一套代码部署到桌面和移动端 rui是一款基于Rust的声明式UI库,它让开发者能够使用同一套代码轻松构建跨桌面和移动端的应用程序。
前端后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考