Vue 3 中 v-for 动态组件 ref 收集失败问题排查与解决

0 阅读4分钟

Vue 3 中 v-for 动态组件 ref 收集失败问题排查与解决

问题描述

在开发部门管理页面的搜索栏功能时,遇到了一个奇怪的问题:在 v-for 循环中渲染的动态组件,无法正确收集到 ref 数组中。

image.png

问题现象

// schema-search-bar.vue
const searchComList = ref([]);

const getValue = () => {
  let dtoObj = {};
  console.log("searchComList", searchComList.value); // 输出: Proxy(Array) {}
  searchComList.value.forEach((component) => {
    dtoObj = { ...dtoObj, ...component.getValue() };
  });
  return dtoObj; // 返回: {}
};

现象:

  • searchComList.value 始终是空数组 []
  • 无法获取到任何子组件的实例
  • 导致搜索功能无法正常工作

代码结构

<template>
  <el-form v-if="schema && schema.properties" :inline="true">
    <el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
      <!-- 动态组件 -->
      <component 
        :ref="searchComList"  <!-- ❌ 问题所在 -->
        :is="SearchItemConfig[schemaItem.option?.comType]?.component" 
        :schemaKey="key"
        :schema="schemaItem">
      </component>
    </el-form-item>
  </el-form>
</template>

<script setup>
const searchComList = ref([]);
</script>

排查过程

1. 初步怀疑:打印时机问题

最初怀疑是打印时机不对,组件还没有挂载完成。但即使使用 nextTick 或在 onMounted 中打印,searchComList.value 仍然是空数组。

2. 对比其他正常工作的代码

在同一个项目中,发现 schema-view.vue 中类似的代码却能正常工作:

<!-- schema-view.vue - ✅ 正常工作 -->
<component 
  v-for="(item, key) in components" 
  :key="key" 
  :is="ComponentConfig[key]?.component" 
  ref="comListRef"  <!-- ✅ 使用字符串形式 -->
  @command="onComponentCommand">
</component>

<script setup>
const comListRef = ref([]);
// comListRef.value 能正确收集到所有组件实例
</script>

3. 发现关键差异

对比两个文件的代码,发现了关键差异:

文件ref 写法结果
schema-view.vueref="comListRef" (字符串)✅ 正常工作
schema-search-bar.vue:ref="searchComList" (绑定对象)❌ 无法收集

根本原因

Vue 3 中 v-for 使用 ref 的机制

在 Vue 3 中,v-for 中使用 ref 时,两种写法的行为完全不同

1. 字符串形式的 ref(自动收集到数组)
<component v-for="item in list" ref="comListRef" />

行为:

  • Vue 会自动将 ref 的值设置为一个数组
  • 数组中的元素按顺序对应 v-for 中的每一项
  • 这是 Vue 3 的特殊处理机制
2. 绑定 ref 对象(不会自动收集)
<component v-for="item in list" :ref="comListRef" />

行为:

  • :ref 绑定的是一个 ref 对象,Vue 会直接赋值
  • v-for 中,不会自动收集到数组
  • 每次循环都会覆盖上一次的值
  • 最终只会保留最后一个组件的引用

官方文档说明

根据 Vue 3 官方文档:

当在 v-for 中使用 ref 时,ref 的值将是一个数组,包含所有循环项对应的组件实例。

关键点: 这个特性只适用于字符串形式的 ref,不适用于 :ref 绑定。

解决方案

方案一:使用字符串形式的 ref(推荐)

<template>
  <el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
    <component 
      ref="searchComList"  <!-- ✅ 去掉冒号,使用字符串形式 -->
      :is="SearchItemConfig[schemaItem.option?.comType]?.component" 
      :schemaKey="key"
      :schema="schemaItem">
    </component>
  </el-form-item>
</template>

<script setup>
const searchComList = ref([]);
// 现在 searchComList.value 会自动收集到所有组件实例
</script>

方案二:使用函数形式的 ref(更灵活)

如果需要更精细的控制(比如去重、按 key 索引等),可以使用函数形式:

<template>
  <el-form-item v-for="(schemaItem, key) in schema.properties" :key="key">
    <component 
      :ref="(el) => handleRef(el, key)"  <!-- ✅ 函数形式 -->
      :is="SearchItemConfig[schemaItem.option?.comType]?.component" 
      :schemaKey="key"
      :schema="schemaItem">
    </component>
  </el-form-item>
</template>

<script setup>
const searchComList = ref([]);
const componentMap = new Map();

const handleRef = (el, key) => {
  if (el) {
    // 如果已经存在,先移除旧的(避免重复)
    if (componentMap.has(key)) {
      const oldIndex = searchComList.value.indexOf(componentMap.get(key));
      if (oldIndex > -1) {
        searchComList.value.splice(oldIndex, 1);
      }
    }
    // 添加新的组件实例
    componentMap.set(key, el);
    searchComList.value.push(el);
  } else {
    // 组件卸载时,从 Map 和数组中移除
    if (componentMap.has(key)) {
      const oldEl = componentMap.get(key);
      const index = searchComList.value.indexOf(oldEl);
      if (index > -1) {
        searchComList.value.splice(index, 1);
      }
      componentMap.delete(key);
    }
  }
};
</script>

技术要点总结

1. Vue 3 ref 在 v-for 中的行为

写法在 v-for 中的行为适用场景
ref="xxx"自动收集到数组✅ 推荐,简单场景
:ref="xxx"不会自动收集,会覆盖❌ 不适用于 v-for
:ref="(el) => fn(el)"手动控制收集逻辑✅ 需要精细控制时

2. 最佳实践

  1. 在 v-for 中使用 ref 时,优先使用字符串形式

    <component v-for="item in list" ref="comListRef" />
    
  2. 如果需要按 key 索引或去重,使用函数形式

    <component v-for="(item, key) in list" :ref="(el) => handleRef(el, key)" />
    
  3. 避免在 v-for 中使用 :ref="refObject"

    <!-- ❌ 不推荐 -->
    <component v-for="item in list" :ref="comListRef" />
    

3. 调试技巧

当遇到 ref 收集问题时,可以:

  1. 检查 ref 的写法:确认是字符串还是绑定对象
  2. 使用 nextTick 延迟检查:确保组件已挂载
  3. 对比正常工作的代码:找出差异点
  4. 查看 Vue DevTools:检查组件实例是否正确创建

相关资源

总结

这个问题看似简单,但实际上涉及到 Vue 3 中 refv-for 中的特殊处理机制。关键点在于:

  1. 字符串形式的 ref 在 v-for 中会自动收集到数组
  2. 绑定形式的 :ref 在 v-for 中不会自动收集
  3. 函数形式的 :ref 可以手动控制收集逻辑

记住这个规则,可以避免很多类似的坑。在开发过程中,如果遇到 ref 收集问题,首先检查是否在 v-for 中使用了错误的 ref 写法。