Code前端首页关于Code前端联系我们

Vue3 里怎么用 ref 数组?操作和响应式处理要注意啥?

terry 2周前 (10-01) 阅读数 40 #Vue
文章标签 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-forref 回调会重新执行,如果用 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 数组问题能快速反应:

  1. 创建:用 ref([]) 初始化,结合 v-forref 回调(优先用“索引赋值”避免重复收集)。
  2. 操作:所有增删改查必须通过 refArr.value 来做,保证响应式。
  3. 避坑:别在 ref 回调里盲目 push,别忘记 .value,区分 ref 数组和 reactive 数组的响应式逻辑。
  4. 场景:批量 DOM 操作、子组件实例管理、动态表单/表格的交互逻辑,都是 ref 数组的主战场。

把这些逻辑吃透,再遇到“列表数据要批量操作”的需求,就不会慌啦~ 要是还有细节没搞懂,评论区随时喊我,咱们再拆解!

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门