Vue 3 的 v-if 和 v-for 优先级详解
本文保留原文对 Vue 2、Vue 3 指令优先级差异的说明,并按当前官方文档修正示例。本文中的 Vue 3 以 Vue 3.5 为基线,Vue 2 以 Vue 2.7 的兼容行为为参考。
一、Vue 2 中为什么不建议同时使用
在 Vue 2 中,如果 v-if 和 v-for 写在同一个元素上,v-for 的优先级高于 v-if。因此下面的表达式可以访问 item 和 index:
<template>
<div class="hello">
<div
v-for="(item, index) in list"
v-if="index === 9"
:key="item"
>
{{ item }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
}
},
}
</script>
可以把它粗略理解为:先遍历 list,再对每一项判断 v-if。即使最后只渲染一项,每次相关数据更新时也要遍历整个列表:
this.list.map((item, index) => {
if (index === 9) {
return item
}
})
这不一定意味着每次都会产生大量 DOM 操作,但表达式本身确实需要逐项判断,条件越稳定、列表越大,越应该避免把筛选工作放在模板的每一项上。
Vue 2 推荐使用计算属性先过滤
<template>
<div class="hello">
<div v-for="item in filteredList" :key="item">
{{ item }}
</div>
</div>
</template>
<script>
export default {
data() {
return {
list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
}
},
computed: {
filteredList() {
return this.list.filter((_, index) => index === 9)
},
},
}
</script>
计算属性会缓存结果,只有依赖变化时才重新计算;模板只负责遍历已经筛选后的数组。筛选条件如果还依赖用户输入,也可以在计算属性中统一处理。
二、Vue 3 的优先级发生了变化
在 Vue 3 中,如果两个指令写在同一个元素上,v-if 的优先级高于 v-for。因此下面的代码不能访问 item 或 index:
<template>
<div
v-for="(item, index) in list"
v-if="index === 9"
:key="item"
>
{{ item }}
</div>
</template>
v-if 会先求值,但 index 只在 v-for 的作用域中才存在。Vue 3 编译器会报错或运行时提示类似:
Property "index" was accessed during render but is not defined on instance.
所以,Vue 3 的结论不是“既然 v-if 优先级更高就可以鼓励同时使用”,而是更应该避免在同一个元素上同时使用二者。
三、Vue 3 正确的写法
1. 根据每一项决定是否渲染:把 v-for 放到 template
<template>
<ul>
<template v-for="(item, index) in list" :key="item.id">
<li v-if="item.show">
{{ index }} - {{ item.name }}
</li>
</template>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const list = ref([
{ id: 1, name: '张三', show: false },
{ id: 2, name: '李四', show: true },
])
</script>
v-for 负责建立循环作用域,v-if 写在循环内部的子节点上,因此可以访问 item。Vue 3 还支持把 :key 放在 <template v-for> 上,key 应该稳定且尽量使用业务 id,而不是在列表会增删时使用 index。
如果每次循环实际只渲染一个根节点,也可以直接把 v-for 放在外层容器上:
<ul v-if="list.length">
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
2. 想在列表为空时跳过整个循环:把 v-if 放到外层
<template>
<div class="hello">
<ul v-if="list.length > 0">
<li v-for="item in list" :key="item.id">
{{ item.name }}
</li>
</ul>
<p v-else>暂无数据</p>
</div>
</template>
这里 v-if 控制的是整个列表容器,条件为 false 时连列表的循环也不会执行。这正是“如果想有条件地跳过循环执行,应把 v-if 放到外层”的场景。
3. 需要筛选列表:使用计算属性或普通函数
<script setup>
import { computed, ref } from 'vue'
const list = ref([
{ id: 1, name: '张三', show: false },
{ id: 2, name: '李四', show: true },
])
const visibleList = computed(() => {
return list.value.filter(item => item.show)
})
</script>
<template>
<ul>
<li v-for="item in visibleList" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
如果筛选逻辑不需要缓存,也可以使用方法;如果只想在模板中格式化当前项,不要为了简单条件再引入复杂抽象。重点是让列表的数据准备和 DOM 结构职责分离。
四、不要把 v-show 当成优先级解决方案
v-if 和 v-show 都可以控制显示,但作用不同:
v-if是真正的条件渲染,条件为 false 时节点和子组件不会创建;切换时会销毁/重新创建对应分支。v-show始终渲染节点,只切换 CSS 的display;适合频繁切换、但初次渲染成本可以接受的场景。v-show不能与v-else、v-else-if配合,也不能解决v-if/v-for的作用域优先级问题。
例如:
<button @click="open = !open">切换</button>
<div v-show="open">频繁显示和隐藏的内容</div>
五、性能和可读性建议
- 不要在同一个元素上同时使用
v-if和v-for。 - 对列表项筛选使用计算属性,把过滤结果直接交给
v-for。 - 控制整个列表是否存在时,把
v-if放到列表容器上。 - 依据单项属性判断时,使用
<template v-for>包裹v-if子节点。 - 列表增删和排序时使用稳定的业务 key,不要无条件使用 index。
- 只有需要频繁切换且希望保留 DOM 时才考虑
v-show。 - 计算属性和过滤函数本身也有成本;数据量特别大时,还应考虑分页、虚拟列表和服务端筛选。
六、结论
- 在 Vue 2 中,同一元素上的
v-for优先级高于v-if。 - 在 Vue 3 中,同一元素上的
v-if优先级高于v-for,循环别名不能在v-if中使用。 - Vue 2、Vue 3 都不推荐在同一元素上混用二者。
- 按单项筛选时使用
<template v-for>;按整体条件控制时把v-if放在外层;复杂筛选使用 computed。
官方参考
原文出处
作者:孤山渐青
原文链接:https://juejin.cn/post/7128328827981266980
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。