- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
本文围绕 VueUse 官方文档 useArrayEvery 展开,系统讲解这一将原生
Array.prototype.every响应式化的组合式函数:它如何基于 Vue 3 的computed与toValue实现数据驱动校验,支持"多 ref 数组"与"响应式数组"两种输入形态,并实时追踪元素变化自动更新判定结果。读者读完可掌握其 API 签名、参数语义、底层实现原理与真实测试验证,并能在表单校验、权限检查等场景中直接落地使用。
一、为什么需要"响应式的 every":从原生方法到组合式函数
在 Vue 3 开发中,我们经常需要判定"一个数组中的所有元素是否都满足某个条件"。原生 JavaScript 提供了Array.prototype.every,它会遍历数组并返回一个布尔值:
const list = [0, 2, 4, 6, 8] const result = list.every(i => i % 2 === 0) // result: true然而,原生every是一次性求值的:它只在调用时刻执行一次,之后数组发生变化,结果并不会自动更新。在 Vue 的响应式体系中,这种"静态判定"无法驱动模板或watch自动更新。
VueUse 给出的答案是useArrayEvery——一个把Array.every变成响应式计算属性的组合式函数。其核心实现位于 packages/shared/useArrayEvery/index.ts,全文仅 21 行:
export function useArrayEvery<T>( list: MaybeRefOrGetter<MaybeRefOrGetter<T>[]>, fn: (element: T, index: number, array: MaybeRefOrGetter<T>[]) => unknown, ): UseArrayEveryReturn { return computed(() => toValue(list).every((element, index, array) => fn(toValue(element), index, array))) }从源码结构可以清晰看出它的设计哲学:
- 返回值是一个
ComputedRef<boolean>(类型别名UseArrayEveryReturn),而非普通布尔值。computed会收集依赖并建立缓存,一旦数组或其元素发生变化,重新求值得到最新结果。 - 输入参数支持
MaybeRefOrGetter:数组本身可以是普通数组、ref,也可以是getter函数;数组中的每个元素同样可以是普通值或ref。这种双层解包的签名是 VueUse 数组系列(useArrayMap、useArrayFilter、useArrayFind等)的统一风格。 - 元素通过
toValue(element)解包后再传入fn,保证回调拿到的始终是"解包后的实际值"。
二、API 签名与参数语义
对照 源码 中的类型定义与 JSDoc 注释,useArrayEvery的完整签名如下:
useArrayEvery<T>( list: MaybeRefOrGetter<MaybeRefOrGetter<T>[]>, fn: (element: T, index: number, array: MaybeRefOrGetter<T>[]) => unknown, ): ComputedRef<boolean>参数说明
| 参数 | 类型 | 说明 |
|---|---|---|
list | MaybeRefOrGetter<MaybeRefOrGetter<T>[]> | 被遍历判定的数组。可以是普通数组、Ref<数组>或返回数组的 getter 函数;数组元素本身也可以是普通值或 ref |
fn | (element, index, array) => unknown | 用于测试每个元素的回调函数。签名与原生Array.every保持一致:element是当前元素(已解包),index是当前索引,array是原始数组(元素可能仍是 ref) |
返回值
- 类型:
ComputedRef<boolean> - 语义:若
fn对数组每一个元素都返回真值,结果为true;只要有一个元素不满足,结果为false。对空数组调用时,Array.prototype.every会返回true(原生语义),useArrayEvery继承这一行为。
需要特别注意的是,回调fn的返回类型是unknown,即结果通过真值判断(truthy/falsy)来汇总,与原生every的布尔化行为一致。
与 useArraySome 的对照
同为数组校验家族成员,useArraySome 实现的是"存在性判定"(任一元素满足即为true),而useArrayEvery是"全量判定"。两者源码结构几乎一致,仅把.every(...)换成.some(...),从源码结构看,VueUse 有意将这类函数做成统一模式的姊妹函数。在业务中,"是否全部满足"与"是否存在满足"常常成对出现,例如权限校验里"所有角色都具备"用useArrayEvery,"至少一个角色具备"用useArraySome。
三、官方用法示例:两种输入形态
useArrayEvery 官方文档 给出了两种典型用法,下面结合注释完整还原并解读。
3.1 使用多 ref 数组(array of multiple refs)
当数组中的每个元素本身都是ref时,useArrayEvery会逐个解包并追踪:
import { useArrayEvery } from '@vueuse/core' const item1 = ref(0) const item2 = ref(2) const item3 = ref(4) const item4 = ref(6) const item5 = ref(8) const list = [item1, item2, item3, item4, item5] const result = useArrayEvery(list, i => i % 2 === 0) // result.value: true —— 所有元素均为偶数 item1.value = 1 // result.value: false —— 1 不是偶数,校验立即失效这里的list是一个普通数组,但数组元素是五个独立的ref。之所以能响应式更新,是因为在computed求值过程中调用了toValue(element),触发了对每个 ref 的依赖收集;当item1.value = 1时,computed依赖失效并自动重算,result.value随之变为false。
3.2 使用响应式数组(reactive array)
当数组本身是ref时,追踪的是整个数组的引用与内容变化:
import { useArrayEvery } from '@vueuse/core' const list = ref([0, 2, 4, 6, 8]) const result = useArrayEvery(list, i => i % 2 === 0) // result.value: true —— 所有元素均为偶数 list.value.push(9) // result.value: false —— 9 打破校验此例中list是Ref<number[]>。toValue(list)读取ref的值并建立依赖,push(9)触发了ref内部值的变化(Vue 3 的 ref 会对.value上的数组变更做响应式包装),从而驱动result重算。注意:这里回调fn拿到的i是普通数字(数组本身是普通值数组,元素无需再解包)。
3.3 输入形态小结
| 输入形态 | 示例 | 依赖追踪目标 | 触发更新的操作 |
|---|---|---|---|
| 普通数组 + 元素为 ref | const list = [item1, item2] | 每个元素的 ref | 修改任一itemX.value |
Ref<数组> | const list = ref([0, 2, 4]) | 数组 ref 本身 | push/splice/ 重新赋值list.value |
| getter 函数 | const list = () => someArray | getter 内部读取的所有响应式依赖 | getter 依赖变化时 |
从签名看,list参数还支持MaybeRefOrGetter中的getter 形态(如() => props.items),这让它可以直接消费 props、其他组合式函数返回值或计算属性,天然适配组件化场景。
四、源码级原理:computed + toValue 的双层解包
要真正理解useArrayEvery,关键在于弄清computed与toValue的协作机制。
4.1 外层:computed 驱动响应式重算
函数整体包在computed(() => ...)中,这意味着:
- 返回值是一个惰性求值的计算属性,只有被读取(如
result.value或在模板中访问)时才执行回调; - 求值过程中读取到的所有响应式依赖都会被记录,任一依赖变化后,下一次访问
result.value返回重算后的新值; - 由于
@__NO_SIDE_EFFECTS__标注与sideEffects: false(见 packages/shared/package.json),该函数被标记为无副作用,可在模块顶层或任意位置安全调用,也不会被摇树优化误伤。
4.2 内层:toValue 逐元素解包
toValue是 Vue 3.3+ 提供的官方解包工具,语义上等价于"若传入 ref 则返回.value,若传入函数则调用之,否则原样返回"。在useArrayEvery中它被调用了两次:
toValue(list).every((element, index, array) => fn(toValue(element), index, array))- 第一次
toValue(list):解包数组本身,支持ref数组或 getter; - 第二次
toValue(element):在.every的回调内部逐元素解包,支持"数组元素是 ref"的形态; - 回调的三参
(element, index, array)中,array参数传递的是解包后的数组但元素仍可能为 ref 的原始数组,这与原生every的"第三参是原数组"语义对齐;fn拿到的element则已经是解包后的实际值。
这种双层解包正是文档中两种用法(多 ref 数组 / 响应式数组)都能工作的底层原因,也让useArrayEvery与 useArrayMap 等姊妹函数共享同一套类型签名模式。
4.3 与手写 watch + every 的对比
如果不使用useArrayEvery,响应式全量校验通常需要手写:
const list = ref([0, 2, 4, 6, 8]) const result = ref(true) watch(list, (val) => { result.value = val.every(i => i % 2 === 0) }, { immediate: true, deep: true })两相对比,useArrayEvery的优势体现在:
- 声明式而非命令式:没有
watch、没有手动同步逻辑,computed天然缓存; - 深响应无需 deep 选项:
computed求值过程中读取的依赖自动收集,元素 ref 与数组内容的变化都能精确触发,无需依赖deep: true的暴力遍历; - 类型推导完整:
ComputedRef<boolean>直接可用于模板与watch,TS 类型安全。
五、测试用例验证:行为即契约
仓库中配套的单元测试 packages/shared/useArrayEvery/index.test.ts 精确复刻了文档中的两种用法,构成对 API 行为的契约验证:
it('should work with array of refs', () => { const item1 = shallowRef(0) // ... item2 ~ item5 均为偶数 const list = [item1, item2, item3, item4, item5] const result = useArrayEvery(list, i => i % 2 === 0) expect(result.value).toBe(true) item1.value = 1 expect(result.value).toBe(false) }) it('should work with reactive array', () => { const list = deepRef([0, 2, 4, 6, 8]) const result = useArrayEvery(list, i => i % 2 === 0) expect(result.value).toBe(true) list.value.push(9) expect(result.value).toBe(false) })从测试细节可以确认两点实现事实:
- 测试分别使用
shallowRef(元素为 ref 的场景)与deepRef(数组为 ref 的场景),证明useArrayEvery对浅层 ref 与深层响应数组都能正确响应; - 测试只断言
result.value的最终布尔值,说明该函数不需要手动 watch 或调用任何"刷新"接口,读取result.value即获得最新判定结果。
六、实战场景与进阶用法
6.1 表单校验:全部字段合法才可提交
import { useArrayEvery } from '@vueuse/core' import { ref } from 'vue' const name = ref('') const email = ref('') const phone = ref('') const fields = [name, email, phone] const isValid = useArrayEvery(fields, v => v.trim().length > 0) // 模板中可直接使用 // <button :disabled="!isValid">提交</button>当任一输入框清空,isValid.value立即变为false,按钮随之禁用——无需任何额外监听代码。
6.2 权限校验:所有角色都满足条件
const roles = ref(['admin', 'editor']) const allActive = useArrayEvery( () => roles.value, r => ['admin', 'editor', 'viewer'].includes(r), )这里list使用 getter 形态() => roles.value,展示了MaybeRefOrGetter的第三种输入方式。
6.3 结合 ref 数组动态增删
const items = ref<number[]>([]) const allEven = useArrayEvery(items, i => i % 2 === 0) // items.value 为空数组时,allEven.value === true(every 对空数组的原生语义) items.value.push(2, 4) // allEven.value: true items.value.push(3) // allEven.value: false6.4 空数组的语义提醒
继承原生Array.prototype.every的语义,空数组调用every恒为true(vacuous truth)。如果你的业务需要"空数组判定为 false",需要在外层自行处理,例如:
const result = computed(() => toValue(list).length > 0 && useArrayEvery(list, fn).value)七、使用前置条件
- Vue 版本:需要 Vue 3.5+(
useArrayEvery依赖computed与toValue,所在包 packages/shared/package.json 的 peerDependencies 声明为vue: ^3.5.0); - 安装方式:该函数从
@vueuse/shared导出(见 packages/shared/index.ts),并已通过@vueuse/core对外统一暴露,因此文档示例中的import { useArrayEvery } from '@vueuse/core'是推荐入口; - 函数归属:位于 VueUse 的
shared包(Array 分类),不依赖浏览器 DOM API,因此在 Node 环境、SSR 场景下均可安全使用。
八、小结
useArrayEvery是 VueUse 对原生Array.prototype.every的一次精炼封装:用computed提供响应式缓存,用toValue完成数组与元素的双层解包,从而以极少的代码量(源码仅 21 行)实现"数据一变、判定即新"的声明式校验。官方文档提供多 ref 数组与响应式数组两种示例,配套测试用例(index.test.ts)与源码(index.ts)共同构成了完整的行为契约。无论是表单全量校验、权限全量检查,还是任何"全部元素满足条件"的业务判定,它都是比手写watch + every更简洁、更符合 Vue 响应式哲学的替代方案。
- 前端
【免费下载链接】vueuse
Collection of essential Vue Composition Utilities for Vue 3
相关推荐
深入解析 airi 项目中的 VueUse useArrayEvery:响应式 Array.every 的完整指南
深入解析 airi 项目中的 VueUse useArrayEvery:响应式 Array.every 的完整指南 导读 useArrayEvery 是 Vue
AI 应用人工智能大模型数字人AI Agent语音前端后端桌面应用移动开发即时通讯3D渲染飞书卡片多图混排 img_combination 组件完全指南:四种拼版模式、字段详解与 CLI 发送实战
飞书卡片多图混排 img_combination 组件完全指南:四种拼版模式、字段详解与 CLI 发送实战 img_combination 是飞书(Lark)C
前端VueUse @vueuse/math useMin:响应式 Math.min 组合式函数完全指南
VueUse @vueuse/math useMin:响应式 Math.min 组合式函数完全指南 useMin 是 VueUse @vueuse/math 扩
前端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考