Vue3 里怎么用 ref 数组?操作和响应式处理要注意啥?
不少刚开始用 Vue3 的同学,在处理列表类数据或者需要批量操作 DOM/组件实例时,总会纠结 ref 数组咋用,比如想给一组输入框加焦点、批量管理自定义组件状态,ref 数组到底咋创建、咋保证响应式,还有操作时要避开哪些坑?今天就把这些问题拆开来唠明白。
啥是 Vue3 的 ref 数组?和普通数组有啥不一样?
先搞清楚“ref 数组”的本质:它是元素全为 ref 对象的数组,而且整个数组得用 ref()
包裹起来(也可以理解成“存 ref 的响应式数组”)。
举个例子,普通数组长这样:const normalArr = [1, 2, 3]
,里面直接存数值;而 ref 数组得是 const refArr = ref([ref(1), ref(2), ref(3)])
—— 数组里每个元素都是 ref()
包过的响应式容器,整个数组也被 ref()
包着,保证数组本身的增删改查能触发页面更新。
和普通数组的核心区别在于响应式能力:普通数组里的数据变了,Vue 不会自动更新页面;但 ref 数组里,只要通过 refArr.value
去修改元素(比如改某个 ref 的 value
,或者给数组增删元素),Vue 就能感知变化,自动更新页面。
咋创建 ref 数组?有哪些常见方式?
创建 ref 数组得结合场景来选方法,常见的有“静态列表场景”和“动态添加场景”两种情况:
静态列表:渲染已知数量的 DOM/组件
比如页面一开始就固定渲染 3 个输入框,要收集它们的 DOM 做批量操作,这时候可以用 v-for
+ ref
回调来创建:
<template> <input v-for="(item, index) in 3" :ref="(el) => { if (el) refArr.value[index] = el }" /> </template> <script setup> import { ref, onMounted } from 'vue' const refArr = ref([]) // 先初始化一个空的 ref 数组 onMounted(() => { console.log(refArr.value) // 能拿到 3 个 input 的 DOM 元素 }) </script>
这里注意 ref
回调的逻辑:只有 DOM 渲染完成后(el
存在时),才把 DOM 放到数组对应索引的位置,如果直接写 refArr.value.push(el)
,组件更新时(比如数据变化导致重新渲染)会重复 push,导致数组长度超出预期,用索引赋值就能避免这个问题~
动态添加:用户操作新增元素(比如动态表单)
假设做一个“点击按钮新增输入框”的功能,同时要跟踪每个输入框的 DOM,这时候得主动往 ref 数组里塞新的 ref 对象:
<template> <button @click="addInput">新增输入框</button> <div v-for="(item, index) in inputRefs.value" :key="index"> <input :ref="(el) => inputRefs.value[index] = el" /> </div> </template> <script setup> import { ref } from 'vue' const inputRefs = ref([]) // 初始化空的 ref 数组 function addInput() { inputRefs.value.push(ref(null)) // 新增一个存 DOM 的 ref(初始值为 null) } </script>
这里的关键是:每次新增元素时,先往 inputRefs.value
里 push 一个空的 ref(null)
,这样 v-for
渲染时,ref
回调能把真实 DOM 绑定到这个新 ref 上,如果不先 push 空 ref,直接在 ref
回调里 push,同样会遇到“重复收集”的坑~
操作 ref 数组时,响应式咋保证?容易踩的坑有哪些?
很多同学明明用了 ref 数组,却发现页面不更新,大概率是操作方式不对,先讲响应式的核心逻辑:ref 数组的响应式,靠的是 refArr.value
这个“入口” —— 所有对数组的增删改查,必须通过 refArr.value
来做,Vue 才能感知变化。
然后重点避坑:
坑 1:v-for 里用 ref 回调时“重复收集 DOM”
前面提过,组件更新时(比如数据变化、父组件重渲染),v-for
的 ref
回调会重新执行,如果用 push
而不是“索引赋值”,数组长度会疯长:
<!-- 错误示范:重复 push 导致数组长度爆炸 --> <input v-for="(item, index) in list" :ref="(el) => refArr.value.push(el)" />
解决方法:用索引精准赋值,保证每个位置只存对应 DOM:
<input v-for="(item, index) in list" :ref="(el) => refArr.value[index] = el" />
坑 2:直接修改 ref 数组,没走 value
新手容易犯的错:把 ref 数组当普通数组用,忘记它是被 ref()
包着的。
<script setup> const refArr = ref([ref(1), ref(2)]) // 错误操作:直接对 refArr 做修改,没通过 .value refArr.push(ref(3)) // 无效!refArr 是 ref 对象,不是数组本身 refArr[0] = ref(0) // 也无效! </script>
正确操作:所有修改都要通过 refArr.value
:
refArr.value.push(ref(3)) // 新增元素,触发响应式 refArr.value[0] = ref(0) // 替换元素,触发响应式
坑 3:把 ref 数组当“普通响应式数组”,忽略 ref 嵌套
有些同学会疑惑:“我用 reactive()
包数组不行吗?” 还真不一样 —— reactive()
对数组的索引赋值不敏感(arr[0] = newVal
不会触发更新),但 ref 数组是通过 ref()
包数组,数组本身的“整体变化”(push、替换元素)能被感知。
举个对比例子:
// reactive 数组:索引赋值不触发更新 const reactiveArr = reactive([ref(1), ref(2)]) reactiveArr[0] = ref(3) // 页面不会更新! // ref 数组:索引赋值会触发更新(因为改的是 ref 的 value) const refArr = ref([ref(1), ref(2)]) refArr.value[0] = ref(3) // 页面会更新!
所以如果需要频繁替换数组元素,用 ref 数组更稳妥;如果是纯数据列表(不需要操作 DOM/组件实例),用 reactive()
包数组+变异方法(push/pop/splice)更合适~
ref 数组在实战中有哪些典型用法?
理解了基础操作,得看看实际开发中哪些场景必须用 ref 数组:
场景 1:批量操作 DOM(比如表单聚焦、清空)
做登录表单时,想让所有输入框一键聚焦?用 ref 数组收集 DOM 后循环调用方法:
<template> <button @click="focusAll">全部聚焦</button> <input v-for="(item, index) in 2" :ref="(el) => inputRefs.value[index] = el" /> </template> <script setup> import { ref } from 'vue' const inputRefs = ref([]) function focusAll() { inputRefs.value.forEach(el => el.focus()) // 每个 input 执行 focus() } </script>
场景 2:管理自定义组件的实例(调用子组件方法)
如果有多个子组件,需要批量调用它们的方法(全部重置”),ref 数组能统一管理:
<!-- 子组件 Child.vue --> <template><div>{{ count }}</div></template> <script setup> import { ref } from 'vue' const count = ref(0) const increment = () => count.value++ defineExpose({ increment }) // 暴露方法给父组件 </script> <!-- 父组件 --> <template> <button @click="callAllIncrement">全部 +1</button> <Child v-for="(item, index) in 3" :ref="(el) => childRefs.value[index] = el" /> </template> <script setup> import { ref } from 'vue' import Child from './Child.vue' const childRefs = ref([]) function callAllIncrement() { childRefs.value.forEach(child => child.increment()) // 调用每个子组件的 increment } </script>
场景 3:动态表单验证(逐字段检查合法性)
做动态表单时,每个输入框的验证逻辑可以通过 ref 数组批量执行:
<template> <button @click="validate">验证所有字段</button> <div v-for="(item, index) in formItems" :key="index"> <input v-model="item.value" :ref="(el) => inputRefs.value[index] = el" :placeholder="item.label" /> </div> </template> <script setup> import { ref, reactive } from 'vue' const formItems = reactive([ { label: '用户名', value: '' }, { label: '密码', value: '' }, { label: '手机号', value: '' } ]) const inputRefs = ref([]) function validate() { let isAllValid = true inputRefs.value.forEach((el, index) => { if (!formItems[index].value) { // 假设简单验证:非空 el.classList.add('error') isAllValid = false } else { el.classList.remove('error') } }) if (isAllValid) alert('验证通过!') } </script>
和 reactive 数组搭配用,能玩出啥花样?
实际项目里,很少只用纯 ref 数组或纯 reactive 数组,更多是“数据用 reactive 管理,交互用 ref 数组跟踪”的组合拳。
比如做一个“可编辑表格”:表格数据存在 reactive
数组里,每个单元格的输入框用 ref 数组跟踪 DOM,实现“批量保存”“批量清空”等操作,数据变化由 reactive
保证响应式,DOM 操作由 ref 数组负责,各司其职又互相配合~
ref 数组的核心要点
最后帮大家理个逻辑链,以后遇到 ref 数组问题能快速反应:
- 创建:用
ref([])
初始化,结合v-for
的ref
回调(优先用“索引赋值”避免重复收集)。 - 操作:所有增删改查必须通过
refArr.value
来做,保证响应式。 - 避坑:别在
ref
回调里盲目 push,别忘记.value
,区分 ref 数组和 reactive 数组的响应式逻辑。 - 场景:批量 DOM 操作、子组件实例管理、动态表单/表格的交互逻辑,都是 ref 数组的主战场。
把这些逻辑吃透,再遇到“列表数据要批量操作”的需求,就不会慌啦~ 要是还有细节没搞懂,评论区随时喊我,咱们再拆解!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。