Vue.js 3中如何简洁动态管理refs元素的添加与移除?
Vue中简洁动态控制元素ref引用状态的方法
问题场景
你尝试用动态绑定:ref="option.selected? 'foobar': null"来控制元素的ref引用,代码如下:
<script setup lang="ts"> import {ref, useTemplateRef} from "vue"; const options = ref([ {label: "One"}, {label: "Two", selected: true}, {label: "Three"}, ]); const foobar = useTemplateRef('foobar'); </script> <template> <span v-for="option in options" :key="option.label" :ref="option.selected? 'foobar': null" @click="option.selected = !option.selected" >{{ option.label }}</span> Selected elements: {{ foobar }} </template>
但发现元素一旦加入foobar refs,取消选中后不会被移除;用v-if/v-else拆分元素虽然能解决问题,但会导致大量重复代码,冗余严重。
问题根源
Vue的字符串式动态ref绑定,仅在元素首次渲染或**被重新创建(比如v-if切换)**时才会注册/移除引用。当你把ref动态设为null时,Vue不会主动从已注册的refs集合中删除对应元素的引用,只会忽略后续的更新。
简洁解决方案
方案1:使用函数式ref手动管理引用集合
放弃字符串ref,改用函数形式的ref,手动维护选中元素的引用数组,这是最直接的解决方式:
<script setup lang="ts"> import {ref} from "vue"; const options = ref([ {label: "One"}, {label: "Two", selected: true}, {label: "Three"}, ]); const foobar = ref<HTMLElement[]>([]); // 手动管理ref的添加与移除 const setFoobarRef = (el: HTMLElement | null, option: typeof options.value[0]) => { if (option.selected && el) { // 避免重复添加 if (!foobar.value.includes(el)) { foobar.value.push(el); } } else if (el) { // 移除对应元素的引用 foobar.value = foobar.value.filter(item => item !== el); } }; </script> <template> <span v-for="option in options" :key="option.label" :ref="(el) => setFoobarRef(el, option)" @click="option.selected = !option.selected" >{{ option.label }}</span> Selected elements: {{ foobar }} </template>
这种方式完全由你控制ref的生命周期,选中状态切换时能实时更新foobar数组,不需要重复写元素代码。
方案2:封装子组件消除代码冗余
如果你的元素结构复杂,重复代码多,可以把可点击的选项封装成独立子组件,内部处理ref的条件逻辑:
<!-- OptionItem.vue --> <script setup lang="ts"> import {ref, watch} from "vue"; const props = defineProps<{ option: {label: string; selected: boolean} }>(); const emit = defineEmits<{ 'update:option': [option: typeof props.option] }>(); const elRef = ref<HTMLElement | null>(null); const foobarRef = defineRef<HTMLElement | null>(); // 监听选中状态,控制ref的赋值 watch(() => props.option.selected, (isSelected) => { foobarRef.value = isSelected ? elRef.value : null; }); const toggleSelected = () => { emit('update:option', {...props.option, selected: !props.option.selected}); }; </script> <template> <span ref="elRef" @click="toggleSelected">{{ props.option.label }}</span> </template>
父组件使用时就非常简洁:
<!-- Parent.vue --> <script setup lang="ts"> import {ref, useTemplateRef} from "vue"; import OptionItem from "./OptionItem.vue"; const options = ref([ {label: "One"}, {label: "Two", selected: true}, {label: "Three"}, ]); const foobar = useTemplateRef('foobar'); </script> <template> <OptionItem v-for="option in options" :key="option.label" :option="option" @update:option="(newOption) => options.value = options.value.map(opt => opt.label === newOption.label ? newOption : opt)" :ref="option.selected ? 'foobar' : null" /> Selected elements: {{ foobar }} </template>
通过组件封装,把重复逻辑收拢到子组件,父组件只需要处理数据传递,既解决了ref更新问题,又消除了代码冗余。
方案3:结合watch手动同步refs(备选)
如果你坚持用字符串ref,可以通过watch监听options的选中状态变化,手动清理无效的引用:
<script setup lang="ts"> import {ref, useTemplateRef, watch} from "vue"; const options = ref([ {label: "One"}, {label: "Two", selected: true}, {label: "Three"}, ]); const foobar = useTemplateRef<HTMLElement[]>('foobar'); // 监听选中状态变化,清理未选中元素的引用 watch(() => options.value.map(opt => opt.selected), () => { if (foobar.value) { // 这里需要结合元素的标识(比如dataset)来判断是否仍处于选中状态 foobar.value = foobar.value.filter(el => { const label = el.textContent; const option = options.value.find(opt => opt.label === label); return option?.selected; }); } }, {deep: true}); </script> <template> <span v-for="option in options" :key="option.label" :ref="option.selected? 'foobar': null" @click="option.selected = !option.selected" >{{ option.label }}</span> Selected elements: {{ foobar }} </template>
这种方式需要依赖元素的内容或自定义属性来匹配选项,不如函数式ref直观,适合简单场景应急。
内容的提问来源于stack exchange,提问作者CJ Dennis
相关产品推荐
相关产品推荐

