- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
导读
useArrayReduce是 VueUse(Vue 3 组合式工具集)中位于packages/shared包内的一个轻量级工具函数,它的职责只有一个:让 Vue 3 的响应式数组获得原生Array.prototype.reduce的聚合计算能力,并且将结果暴露为一个自动更新的ComputedRef。本文以 useArrayReduce 官方文档 为主体,结合其 源码实现 与 单元测试,完整讲解它的三种典型用法(基本用法、响应式数组、initialValue 初值),并深入到类型签名与computed+toValue的实现原理。读完本文,你将掌握在 Vue 组件中写出「数据一变、聚合结果自动刷新」的声明式求值代码。
什么是 useArrayReduce
原生 JavaScript 的Array.prototype.reduce是数组聚合计算(求和、求积、拼接、计数、扁平化等)的标准工具。但它有一个天然的缺陷:它只执行一次。当数组内容在运行时发生变化(例如用户向购物车列表添加商品),你必须手动重新调用一次reduce,聚合结果才能跟上最新数据。
useArrayReduce将这个问题封装为一次性的声明式调用:传入一个数组(可以是ref、reactive数组或 getter 函数),以及一个 reducer 回调,它会返回一个ComputedRef,只要数组内容或其内部元素发生变化,聚合结果就会自动重新计算。
从 packages/shared/index.ts 可以看到,useArrayReduce与useArrayMap、useArrayFilter等函数一同从@vueuse/shared包导出;同时@vueuse/core也会再导出整个 shared 包的内容,因此你在业务代码中既可以从@vueuse/core导入,也可以从@vueuse/shared导入,两者等价。
基本用法:响应式求和
官方文档给出的第一个示例,是把一个由ref元素组成的数组与 reducer 组合起来:
import { useArrayReduce } from '@vueuse/core' const sum = useArrayReduce([ref(1), ref(2), ref(3)], (sum, val) => sum + val) // sum.value: 6这里有两个值得注意的细节:
- 数组元素可以是
ref。useArrayReduce的签名是MaybeRefOrGetter<MaybeRefOrGetter<T>[]>,即数组本身和数组里的每个元素都允许是 ref、getter 或普通值。因此当你后续修改ref的值时,聚合结果会自动重算。 - 返回的是
ComputedRef。sum不是普通数字,必须通过sum.value访问结果,这也意味着它可以被模板直接解包使用,也可以作为其他computed的依赖。
这一点在 单元测试 中得到了印证:
const item1 = shallowRef(1) const item2 = shallowRef(2) const sum = useArrayReduce([item1, item2, 3], (a, b) => a + b) expect(sum.value).toBe(6) item1.value = 4 expect(sum.value).toBe(9) item2.value = 3 expect(sum.value).toBe(10)测试中的第三个数3是普通常量,它同样参与计算;而前两个元素在被shallowRef包裹后,每次赋值都会触发sum.value的重新求值——这正是「响应式 reduce」的核心价值。
与 reactive 数组配合使用
官方文档的第二个示例展示了直接传入reactive数组的场景:
import { useArrayReduce } from '@vueuse/core' const list = reactive([1, 2]) const sum = useArrayReduce(list, (sum, val) => sum + val) list.push(3) // sum.value: 6与基本用法不同,这里传入的list是一个reactive数组,因此push、splice、shift 等所有结构变化都会触发重新计算,无需任何手动同步。测试用例同样覆盖了这一点(index.test.ts):
const list = reactive([1, 2]) const sum = useArrayReduce(list, (a, b) => a + b) expect(sum.value).toBe(3) list.push(3) expect(sum.value).toBe(6)这使useArrayReduce非常适合用于购物车总价、批量勾选统计、多选列表合计等场景:状态变更之后,聚合结果天然保持同步。
传入 initialValue 初始值
官方文档的第三个示例演示了如何指定初始值:
import { useArrayReduce } from '@vueuse/core' const list = reactive([{ num: 1 }, { num: 2 }]) const sum = useArrayReduce(list, (sum, val) => sum + val.num, 0) // sum.value: 3当数组元素是对象、且 reducer 需要从对象中提取字段(如val.num)进行累加时,必须提供initialValue,否则第一次回调时sum会是第一个元素本身,类型与数值计算都会出问题。
结合源码,initialValue还有两个进阶行为需要说明:
- 初值本身也可以是响应式或函数。
useArrayReduce的重载签名将第三参数定义为MaybeRefOrGetter<U>,也就是说你可以传入ref或 getter。在 实现代码 中,初值会通过typeof args[0] === 'function' ? toValue(args[0]()) : toValue(args[0])被解包:如果传的是函数则先调用再解包,否则直接toValue。测试中就用一个返回空数组的函数作为初值,实现了「收集元素字段」的效果(index.test.ts):
const sum = useArrayReduce(list, (prev, val) => { prev.push(val.num) return prev }, (() => []) as unknown as number[]) expect(sum.value).toEqual([1, 2]) list.push({ num: 3 }) expect(sum.value).toEqual([1, 2, 3])- 初值是否传参与 reduce 的行为差异被保留。原生
reduce在没有初始值时,会把数组第一个元素作为累加器的初始值、并从第二个元素开始回调;useArrayReduce在实现时用args.length区分了这两种情况(index.ts),因此它和原生Array.reduce的空数组抛错、索引偏移等语义完全一致,不会因为封装而改变行为。
类型签名与实现原理
两个重载:泛型约束保证类型安全
useArrayReduce通过 TypeScript 重载提供了两套签名(index.ts):
- 无初值重载:
useArrayReduce<T>(list, reducer): UseArrayReduceReturn<T>,要求 reducer 的三个参数类型一致(UseArrayReducer<T, T, T>); - 有初值重载:
useArrayReduce<T, U>(list, reducer, initialValue): UseArrayReduceReturn<U>,累加器类型U与元素类型T可以不同,这正好支撑了「以数组为累加器去收集对象字段」这类异构用法。
reducer 的类型定义为:
export type UseArrayReducer<PV, CV, R> = (previousValue: PV, currentValue: CV, currentIndex: number) => R值得注意的是,回调的第三个参数currentIndex(当前索引)在文档示例中未使用,但它被完整保留,意味着你可以在聚合时感知元素位置,例如实现「带索引的加权求和」。
底层实现:computed + toValue
核心实现只有寥寥数行(index.ts):
export function useArrayReduce<T>( list: MaybeRefOrGetter<MaybeRefOrGetter<T>[]>, reducer: ((...p: any[]) => any), ...args: any[] ): UseArrayReduceReturn<T> { const reduceCallback = (sum: any, value: any, index: number) => reducer(toValue(sum), toValue(value), index) return computed(() => { const resolved = toValue(list) return args.length ? resolved.reduce(reduceCallback, typeof args[0] === 'function' ? toValue(args[0]()) : toValue(args[0])) : resolved.reduce(reduceCallback) }) }拆解其原理:
toValue统一解包:外层toValue(list)把数组本身(可能是 ref/getter)解包为真实数组;内层reduceCallback在调用 reducer 前,对累加器sum和当前元素value分别再执行一次toValue,从而支持「数组元素也是 ref」的嵌套响应式场景。computed负责响应式追踪:整个 reduce 过程被包在computed(() => ...)中。由于computed求值时读取了响应式数组及其元素,Vue 的依赖收集机制会自动记录这些依赖,之后任何一次变更都会触发重算。函数还标注了@__NO_SIDE_EFFECTS__,提示打包器此函数无副作用,可放心做摇树优化。- 对原生语义的忠实保留:如前所述,通过
args.length判断是否传入初值,完美复刻了原生reduce有无初值的两种执行路径。
实用注意事项
- 空数组行为与原生一致:不给初值时对空数组调用
reduce会抛出TypeError(原生数组也有此行为);若需要安全地处理可能为空的数组,请务必传入initialValue(如0、''或[])。 - 不要有副作用:reducer 应当是纯函数。虽然测试中的「数组初值 + push」示例在概念上可行,但每次重算都会基于同一初始数组对象累积,若初值是被复用的引用,可能产生预期外的累积效果,建议像测试那样用工厂函数(
() => [])生成全新初值。 - 与
useArrayMap、useArrayFilter的关系:VueUse 的 shared 包中还有 useArrayMap、useArrayFilter 等一系列数组工具,useArrayReduce与它们遵循相同的「传入响应式数组、返回ComputedRef」约定,可以放心组合使用(例如先useArrayFilter过滤再useArrayReduce求和)。 - 适用的 Vue 版本:
useArrayReduce面向 Vue 3,依赖 Vue 3 的computed与toValueAPI(toValue自 Vue 3.3 起提供),并在 VueUse 的packages/shared与@vueuse/core聚合包中均可直接导入。
小结
useArrayReduce用十几行代码把原生Array.reduce升级为完全响应式的版本:无论数组本身是reactive还是由ref元素构成,也无论是否提供(甚至可以由函数动态生成的)初始值,聚合结果都会随数据源自动更新。理解它的类型重载与computed+toValue实现后,你可以在购物车合计、筛选后统计、批量数据处理等场景中,写出更简洁、更不容易出错的声明式聚合逻辑。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
VueUse useArrayReduce 使用指南:将原生 Array.reduce 变为响应式聚合计算
VueUse useArrayReduce 使用指南:将原生 Array.reduce 变为响应式聚合计算 useArrayReduce 是 VueUse 提供
前端airi 项目实战:深入 VueUse `useArrayReduce` 响应式归约 API 的用法与原理
airi 项目实战:深入 VueUse useArrayReduce 响应式归约 API 的用法与原理 本篇技术指南以 useArrayReduce.md ht
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染IPTVnator 依赖安全覆盖(Dependency Security Overrides)实践:pnpm.overrides 修补传递性 CVE 的正确姿势
IPTVnator 依赖安全覆盖(Dependency Security Overrides)实践:pnpm.overrides 修补传递性 CVE 的正确姿势
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考