news 2026/10/5 2:10:11

VueUse useArrayUnique 详解:响应式数组去重组合式函数的实现与实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VueUse useArrayUnique 详解:响应式数组去重组合式函数的实现与实战
  • 前端

【免费下载链接】vueuse

Collection of essential Vue Composition Utilities for Vue 3

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

useArrayUnique是 VueUse 在@vueuse/core中提供的数组类组合式函数之一,它把数组去重(unique)逻辑封装进 Vue 的响应式系统:只要传入的数组或数组元素发生变化,去重结果就会自动重新计算。本文将以 skills/vueuse-functions/references/useArrayUnique.md 为核心骨架,结合仓库中该函数的真实源码、测试用例与导出入口,全面讲解其用法、参数语义、实现原理与适用边界,读完即可在 Vue 3 / Nuxt 3 项目中直接落地使用。

一、为什么需要"响应式"去重

普通 JavaScript 去重很简单,但把去重放进 Vue 组合式 API 的世界里,需求就变成了三个层次的响应式:

  1. 数组本身可能是 ref:const list = ref([1, 2, 2, 3]),当list.value被重新赋值时要重新去重;
  2. 数组元素可能各自是 ref:const list = [ref(0), ref(1), ref(1)],当某个ref的值变化时要重新去重;
  3. 数组可能是 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>

对每个参数逐一说明:

项类型说明
listMaybeRefOrGetter<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

项目地址:https://gitcode.com/gh_mirrors/vu/vueuse
点击查看免费下载

相关推荐

上一篇:AssetStudio终极指南:如何轻松提取Unity游戏资源
下一篇:3步掌握Chrome独立代理设置:突破系统级限制的专业指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 2:09:14

十分钟跑通首次多智能体股票分析:TradingAgents-CN 零基础实战

十分钟跑通首次多智能体股票分析&#xff1a;TradingAgents-CN 零基础实战 【免费下载链接】TradingAgents-CN 基于多智能体LLM的中文金融交易框架 - TradingAgents中文增强版 项目地址: https://gitcode.com/GitHub_Trending/tr/TradingAgents-CN 抓行情、算指标、扫新…

作者头像 李华
网站建设 2026/10/5 2:08:19

LX Music:免费听歌、歌单存在本地的开源多音源音乐播放器

LX Music&#xff1a;免费听歌、歌单存在本地的开源多音源音乐播放器 【免费下载链接】lx-music-desktop 一个基于 Electron 的音乐软件 项目地址: https://gitcode.com/GitHub_Trending/lx/lx-music-desktop 同一首歌在一个平台下架了&#xff0c;换个平台还有音源可播…

作者头像 李华
网站建设 2026/10/5 2:08:11

AGV与服务机器人主控选型:RK3588/RK3576/RK3568迁移与BOM成本优化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/5 2:06:36

DeepLabCut 数据后处理实战:自动标记身体部位距离异常的帧

人工智能深度学习计算机视觉科研 【免费下载链接】DeepLabCut Official implementation of DeepLabCut: Markerless pose estimation of user-defined features with deep learning for all animals incl. humans 项目地址&#xff1a; https://gitcode.com/gh_mirrors/de/Deep…

作者头像 李华
网站建设 2026/10/5 2:05:21

【NebulaGraph】如何为 NebulaGraph 的存储引擎增加一个新的 Compaction 策略?

NebulaGraph 3.8.0 存储引擎 Compaction 策略深度定制:从源码集成到生产调优的全链路解析 引言:问题界定与场景引入 本文将深入解析用户提出的 “如何为 NebulaGraph 的存储引擎增加一个新的 Compaction 策略?” 这一核心问题。Compaction(压缩/合并)是基于 LSM-Tree 架…

作者头像 李华