- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
useArrayUnique是 VueUse 在@vueuse/core中提供的数组类组合式函数之一,它把数组去重(unique)逻辑封装进 Vue 的响应式系统:只要传入的数组或数组元素发生变化,去重结果就会自动重新计算。本文将以 skills/vueuse-functions/references/useArrayUnique.md 为核心骨架,结合仓库中该函数的真实源码、测试用例与导出入口,全面讲解其用法、参数语义、实现原理与适用边界,读完即可在 Vue 3 / Nuxt 3 项目中直接落地使用。
一、为什么需要"响应式"去重
普通 JavaScript 去重很简单,但把去重放进 Vue 组合式 API 的世界里,需求就变成了三个层次的响应式:
- 数组本身可能是 ref:
const list = ref([1, 2, 2, 3]),当list.value被重新赋值时要重新去重; - 数组元素可能各自是 ref:
const list = [ref(0), ref(1), ref(1)],当某个ref的值变化时要重新去重; - 数组可能是 reactive 对象:
const list = reactive([...]),执行push、splice等操作后要自动反映到结果。
useArrayUnique同时覆盖这三种形态,返回一个ComputedRef<T[]>。从源码结构看,它属于 packages/shared/useArrayUnique/index.ts,通过 packages/shared/index.ts 的export * from './useArrayUnique'统一导出到@vueuse/core入口,并在 skills/vueuse-functions/SKILL.md 的 Array 分类中被标记为AUTO调用级别——也就是说,只要符合"响应式数组去重"的需求,就可以直接使用而无需额外声明。
二、三种基础用法
2.1 使用多个 ref 组成的数组
这是useArrayUnique最典型的场景:数组里的每个元素都是独立的ref,它们各自随时可能变化:
import { useArrayUnique } from '@vueuse/core' const item1 = ref(0) const item2 = ref(1) const item3 = ref(1) const item4 = ref(2) const item5 = ref(3) const list = [item1, item2, item3, item4, item5] const result = useArrayUnique(list) // result.value: [0, 1, 2, 3] item5.value = 1 // result.value: [0, 1, 2]初始去重结果为[0, 1, 2, 3](重复的1只保留一个);当item5的值从3变为1后,数组中出现了三个1,结果自动收缩为[0, 1, 2]——整个更新过程无需手动调用任何函数,全部由组合式函数的响应式依赖自动驱动。
2.2 使用 reactive 数组
直接传入一个reactive数组,后续对数组的增删改同样会触发结果更新:
import { useArrayUnique } from '@vueuse/core' const list = reactive([1, 2, 2, 3]) const result = useArrayUnique(list) // result.value: [1, 2, 3] list.push(1) // result.value: [1, 2, 3]push(1)之后数组中1的计数变为 2,但由于1已经存在于结果中,去重结果维持[1, 2, 3]不变。值得注意的是result与list是单向派生关系:修改result.value不会回写到原始数组。
2.3 使用自定义比较函数
默认去重基于严格相等语义,对于对象数组(尤其是同id不同实例的对象),需要传入自定义比较函数compareFn。比较函数签名与Array.prototype.some的回调类似,接收三个参数:
import { useArrayUnique } from '@vueuse/core' const list = reactive([ { id: 1, name: 'foo' }, { id: 2, name: 'bar' }, { id: 1, name: 'baz' }, ]) const result = useArrayUnique(list, (a, b) => a.id === b.id) // result.value: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }] list.push({ id: 1, name: 'qux' }) // result.value: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]{ id: 1, name: 'baz' }与{ id: 1, name: 'foo' }因id相同而被判定为重复,只保留先出现的元素{ id: 1, name: 'foo' };后续再push一个id: 1的新对象也不会影响结果。
三、参数与返回值:类型声明逐项拆解
useArrayUnique的完整类型声明如下(与 packages/shared/useArrayUnique/index.ts 中的声明一致):
export type UseArrayUniqueReturn<T = any> = ComputedRef<T[]> export declare function useArrayUnique<T>( list: MaybeRefOrGetter<MaybeRefOrGetter<T>[]>, compareFn?: (a: T, b: T, array: T[]) => boolean, ): UseArrayUniqueReturn<T>对每个参数逐一说明:
| 项 | 类型 | 说明 |
|---|---|---|
list | MaybeRefOrGetter<MaybeRefOrGetter<T>[]> | 被去重的数组来源。MaybeRefOrGetter意味着它可以是原始数组、ref包裹的数组或返回数组的 getter 函数;同时数组内部的每个元素也可以是 ref 或 getter,形成"双重解包" |
compareFn | (a, b, array) => boolean | 可选的自定义相等判定函数。a是候选新元素,b是已收入结果中的元素,array是完整解析后的数组。省略时使用默认去重逻辑 |
| 返回值 | ComputedRef<T[]> | 一个计算属性 ref,通过.value读取去重后的数组,读取时自动追踪全部依赖 |
类型参数T支持泛型推断:传入reactive([{ id: 1 }, ...])时,返回值的类型会自动推断为ComputedRef<{ id: number }[]>,无需手动标注。
四、源码级实现原理
useArrayUnique的实现非常精简,核心逻辑全部集中在 packages/shared/useArrayUnique/index.ts 中,仅 30 余行:
function uniq<T>(array: T[]) { return Array.from(new Set(array)) } function uniqueElementsBy<T>( array: T[], fn: (a: T, b: T, array: T[]) => boolean, ) { return array.reduce<T[]>((acc, v) => { if (!acc.some(x => fn(v, x, array))) acc.push(v) return acc }, []) } export function useArrayUnique<T>( list: MaybeRefOrGetter<MaybeRefOrGetter<T>[]>, compareFn?: (a: T, b: T, array: T[]) => boolean, ): UseArrayUniqueReturn<T> { return computed<T[]>(() => { const resolvedList = toValue(list).map(element => toValue(element)) return compareFn ? uniqueElementsBy(resolvedList, compareFn) : uniq(resolvedList) }) }这里有四个值得深入理解的设计点:
4.1 双重toValue解包
toValue(list)负责把"外层"的MaybeRefOrGetter(ref 数组或 getter)解析为普通数组;随后的.map(element => toValue(element))再把数组中的每一个元素逐一解包。正是这一步支撑了"数组元素各自是 ref"的用法——[ref(0), ref(1), ref(1)]经过双重解包后变成[0, 1, 1]再参与去重。
4.2 默认路径:Set去重
uniq使用Array.from(new Set(array))完成去重。Set采用SameValueZero相等语义,因此有两个特性值得留意:
NaN可以被正确去重(NaN === NaN在严格比较中为false,但在Set语义下视为相同);- 对象按引用比较:两个内容相同但引用不同的对象会被视为不同元素,这正是需要自定义
compareFn的场景。
4.3 自定义路径:reduce+some线性扫描
uniqueElementsBy用Array.prototype.reduce维护一个结果累加器acc,对每个新元素v,通过acc.some(x => fn(v, x, array))判断是否已存在"等价"元素;只有不存在时才push。从中可以推断出两个行为约定:
- 保留首次出现的元素:重复元素永远不会替换累加器中已存在的元素;
- 比较函数收到的是完整解析后的数组:第三个参数
array为整个输入数组,可用于实现"与全数组比较"的更复杂判重逻辑。
4.4 惰性求值与@__NO_SIDE_EFFECTS__
函数整体被包裹在computed中,结果是惰性求值的:只有读取result.value时才执行去重,且依赖(list及其元素)变化后仅在下次读取时重算。此外函数声明上方带有@__NO_SIDE_EFFECTS__注释标记,这是面向打包器的纯函数提示,允许 tree-shaking 工具安全地移除未使用的调用,从构建层面保证@vueuse/core的按需引入体积可控。
五、测试用例如何验证行为
仓库为useArrayUnique提供了完整的 Vitest 测试,见 packages/shared/useArrayUnique/index.test.ts,三个用例恰好覆盖上文三种用法:
it('should be defined', () => { expect(useArrayUnique).toBeDefined() }) it('should work with array of refs', () => { const item1 = shallowRef(0) // ... const result = useArrayUnique(list) expect(result.value.length).toBe(4) item5.value = 2 expect(result.value.length).toBe(3) }) it('should work with ref array', () => { const list = deepRef([1, 2, 2, 3]) const result = useArrayUnique(list) expect(result.value.length).toBe(3) list.value.push(1) expect(result.value.length).toBe(3) }) it('should work with array of reactive and custom compare function', () => { // ...reactive 对象数组 + 自定义比较函数 expect(result.value.length).toBe(2) })测试细节值得注意:测试中使用shallowRef包裹数组元素、用deepRef(即ref)包裹数组本身,验证了"元素变化触发重算"与"数组内容变更触发重算"两条响应式链路;result.value.length的断言方式也印证了返回值确实是一个需要读取.value的ComputedRef。
六、适用场景与使用建议
结合 skills/vueuse-functions/SKILL.md 中 Array 分类的编排(useArrayUnique与useArrayDifference、useArrayFilter、useArrayFind等同属一族),可以从几个角度选择性地使用它:
推荐场景
- 标签/关键词列表:多个独立 ref 的状态项汇总去重后渲染;
- 下拉选项、筛选项集合:来自接口或用户交互的重复数据自动归一;
- 对象列表按业务主键(如
id、code)去重:配合自定义compareFn,比手写Map+ 循环更简洁。
注意事项(由实现推导)
- 单向派生:
result是list的派生视图,修改result.value不会回写原数组,如需反向同步可考虑与syncRef等其他组合式函数配合; - 对象默认按引用判重:去重对象数组必须传
compareFn,否则每个对象都会被保留; - 自定义比较函数的稳定语义:比较函数应满足等价关系的自反、对称、传递性质,且结果依赖元素首次出现顺序;
- 适合中小规模数组:自定义比较路径为 O(n²) 的线性扫描(
some每次遍历已收集元素),大规模数据(数千以上)建议自行用Map优化,或仅在输入规模可控时使用。
七、总结
useArrayUnique用一个computed+ 双重toValue+ 两种去重算法(Set与reduce)的组合,把 Vue 世界里"形态各异的响应式数组"统一收敛成去重视图。它既是理解 VueUse Array 系列组合式函数统一设计模式的绝佳样本,也是实际项目中处理标签、选项、主键去重等需求时可以直接信任的工具。官方文档与完整源码位于 packages/shared/useArrayUnique/index.md,类型定义与实现可随时对照 packages/shared/useArrayUnique/index.ts 深入阅读。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useArrayUnique 实战指南:Vue 3 响应式数组去重的完整实现与用法
VueUse useArrayUnique 实战指南:Vue 3 响应式数组去重的完整实现与用法 导读 useArrayUnique 是 VueUse 中用于处
前端VueUse useCeil 详解:响应式 Math.ceil 组合式函数的原理与实战
VueUse useCeil 详解:响应式 Math.ceil 组合式函数的原理与实战 useCeil 是 VueUse @vueuse/math 扩展包中提供
前端airi 项目中的 useArrayUnique 实战指南:VueUse 响应式数组去重 Composable 的用法与原理
airi 项目中的 useArrayUnique 实战指南:VueUse 响应式数组去重 Composable 的用法与原理 本篇技术指南聚焦 VueUse 的
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考