Vue 性能优化:为什么不建议把 v-if 和 v-for 写在同一个元素上?
这篇文章的核心建议仍然成立:不要在同一个元素上同时使用
v-if和v-for。但原因必须按 Vue 2 和 Vue 3 分开说明,因为两代的指令优先级相反。
- Vue 2.7:同一元素上
v-for优先于v-if;- Vue 3.5:同一元素上
v-if优先于v-for,因此条件不能访问v-for的迭代别名;- 两个版本都推荐把条件移到外层、使用 computed 过滤,或使用
<template v-for>。

一、同一个元素上同时使用的问题
原文示例:
<template>
<div v-for="(item, index) in list" v-if="index === 9" :key="item">
{{ item }}
</div>
</template>
这段代码的目标是从十项数据中只显示索引为 9 的第十项。问题不只是“一个判断很耗性能”,还包括:
- 指令优先级随 Vue 版本变化;
- 代码阅读者不容易判断条件作用于列表还是单项;
- 条件依赖迭代变量时,在 Vue 3 中会直接遇到变量不可用;
- 如果列表很大,仍然需要遍历源列表并为每项进行条件判断;
- 列表筛选、空状态和组件卸载逻辑混在模板中,维护成本更高。
Vue 编译器通常可以解析这两个指令;“VS Code 直接报错不能同时使用”更多可能来自 ESLint/Volar 等规则或项目约定,不应描述成 Vue 核心一律禁止编译。即使能够编译,也不代表这种写法是推荐写法。
二、Vue 2 和 Vue 3 的优先级差异
Vue 2.7:v-for 优先
在 Vue 2 中,同一个元素上的 v-for 会先建立迭代上下文,因此 v-if 可以访问 item 和 index:
<!-- Vue 2.7:可以访问 index,但仍不推荐这样写 -->
<li
v-for="(item, index) in list"
v-if="index === 9"
:key="item.id"
>
{{ item.title }}
</li>
它的效果近似于:
先遍历 list
├─ 第 1 项:判断 v-if
├─ 第 2 项:判断 v-if
├─ ...
└─ 第 10 项:判断 v-if
原文将 index === 9 展开成 0 === 10 到 9 === 10,条件写错了:索引从 0 开始,正确的最后一次判断应是 9 === 9。实际项目不要手工展开模板,使用 computed 过滤更清晰。
Vue 3.5:v-if 优先
Vue 3 中,同一个元素上的 v-if 优先于 v-for。因此以下代码中的 item 和 index 在 v-if 求值时还没有建立:
<!-- Vue 3:不要这样写,v-if 不能依赖同节点的 item/index -->
<li
v-for="(item, index) in list"
v-if="item.visible"
:key="item.id"
>
{{ item.title }}
</li>
Vue 3 官方文档明确建议避免在同一元素上混用,因为 v-if 没有访问 v-for 作用域的能力。网上一些 Vue 3.0/3.2 示例能运行,不能说明它们适用于所有 Vue 3 版本和编译器配置。
版本对照
| 写法 | Vue 2.7 | Vue 3.5 |
|---|---|---|
| 同节点指令优先级 | v-for 高于 v-if | v-if 高于 v-for |
v-if 能否访问同节点 item | 可以,但不推荐 | 不可以 |
| 推荐做法 | 外层条件、computed、template v-for | 外层条件、computed、template v-for |
| 性能建议 | 不要对大列表重复模板判断 | 不要依赖同节点优先级,直接重构模板 |
三、正确写法一:条件与列表无关时,把 v-if 放到外层
如果条件判断的是“整个列表是否显示”,不要让每一项都判断一次:
<template>
<ul v-if="showTodos">
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
<p v-else>暂不显示待办事项</p>
</template>
// Vue 2.7
export default {
data() {
return {
showTodos: true,
todos: [
{ id: 1, title: '读文档' },
{ id: 2, title: '写代码' },
],
}
},
}
<!-- Vue 3.5 的 script setup 等价数据 -->
<script setup lang="ts">
import { ref } from 'vue'
const showTodos = ref(true)
const todos = ref([
{ id: 1, title: '读文档' },
{ id: 2, title: '写代码' },
])
</script>
这里 v-if 只判断一次,v-for 只负责遍历真正需要渲染的列表。原文的方法一中使用了没有声明的 x:
<!-- 错误示例:x 没有在 data、props 或 setup 中声明 -->
<div v-if="x === 1">
应改为已声明的 showTodos、canView 等变量。
四、正确写法二:条件依赖每一项时,用 computed 过滤
如果条件依赖 item 或 index,先得到过滤后的数组:
Vue 3.5
<script setup lang="ts">
import { computed, ref } from 'vue'
const todos = ref([
{ id: 1, title: '读文档', done: false },
{ id: 2, title: '写代码', done: true },
{ id: 3, title: '做复盘', done: false },
])
const activeTodos = computed(() => {
return todos.value.filter(todo => !todo.done)
})
</script>
<template>
<ul>
<li v-for="todo in activeTodos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
</template>
Vue 2.7
export default {
data() {
return {
todos: [
{ id: 1, title: '读文档', done: false },
{ id: 2, title: '写代码', done: true },
],
}
},
computed: {
activeTodos() {
return this.todos.filter(todo => !todo.done)
},
},
}
<ul>
<li v-for="todo in activeTodos" :key="todo.id">
{{ todo.title }}
</li>
</ul>
computed 的优势是:
- 模板只遍历已经满足条件的列表;
- 依赖没有变化时可以复用计算结果;
- 过滤逻辑有独立名称,容易测试和复用;
- 组件结构不再把循环和条件嵌套在同一个元素上。
但不要把它夸大成“computed 过滤成本一定远低于 v-if”。过滤本身通常仍是 O(n);它的价值是缓存派生结果、减少不必要的 VNode/DOM 工作和改善表达方式。数据源每次变化时仍需重新过滤。
如果要排序,不要直接修改响应式源数组:
const sortedTodos = computed(() => {
return [...todos.value].sort((a, b) => a.id - b.id)
})
五、正确写法三:使用 <template v-for>
如果必须保留每一项的条件渲染,可以把 v-for 放到 <template>,把 v-if 放到实际元素上。
Vue 3.5
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">
{{ todo.title }}
</li>
</template>
Vue 3 中,key 放在带有 v-for 的 <template> 上。
Vue 2.7
<template v-for="todo in todos">
<li v-if="!todo.done" :key="todo.id">
{{ todo.title }}
</li>
</template>
Vue 2 的旧编译器更常把 key 放在实际渲染的元素上。迁移时不要只复制 Vue 3 的 key 位置而忽略项目版本。
不过,如果只是简单过滤列表,computed 通常比在模板中保留逐项 v-if 更容易维护。
六、v-if 与 v-show 的取舍
这与 v-if/v-for 优先级是两个不同的问题。
v-if
- 条件为假时不创建 DOM 和子组件;
- 切换时创建/销毁节点、监听器和组件实例;
- 适合低频切换或初始条件为假的昂贵子树;
- 与异步组件结合可以延迟到真正需要渲染时加载。
v-show
- 初次渲染就创建 DOM 和组件;
- 通过
display切换可见性; - 适合频繁显示/隐藏且初始化成本高、希望保留 DOM 的场景;
- 不能用来表达“组件完全不需要加载”。
<!-- 频繁开关的小区域可以考虑 v-show -->
<HeavyPanel v-show="visible" />
<!-- 初始不需要且初始化成本高,可以使用 v-if -->
<HeavyPanel v-if="visible" />
七、key 的正确选择
原文数字数组使用 :key="item" 恰好能工作,因为数字唯一且是 primitive。通用业务列表应使用稳定、唯一的 ID:
<li v-for="todo in todos" :key="todo.id">
{{ todo.title }}
</li>
不要:
- 把对象本身作为 key;
- 在列表顺序会变化时无条件使用 index;
- 使用会随内容变化的随机值;
- 多个列表项使用重复 key。
稳定 key 可以帮助 Vue 正确复用组件状态、输入框焦点和 DOM 节点。
八、大列表的进一步优化
如果列表有数千甚至数万条数据,仅仅把 v-if 改成 computed 仍然不够:
- 使用分页或服务端筛选;
- 使用虚拟列表,只渲染可视区域;
- 保持稳定 key;
- 避免在模板中重复执行昂贵函数;
- 控制响应式对象的深度和更新范围;
- 对搜索输入使用防抖;
- 检查组件是否因不必要的依赖而重复渲染。
性能优化应通过性能面板和实际数据量验证,不能只根据指令名称猜测成本。
九、面试式回答
如果面试官问“为什么不建议 v-if 和 v-for 同时使用”,可以这样回答:
两个指令写在同一元素上会产生版本相关的优先级和作用域问题。Vue 2 中
v-for优先,虽然v-if可以访问item,但会对遍历出来的每一项执行条件判断;Vue 3 中v-if优先,条件无法访问同一节点的v-for别名。两代都建议把与列表无关的条件放到外层,把依赖单项的条件放到 computed 中过滤,或者使用<template v-for>明确包裹结构。大型列表还需要结合分页和虚拟列表。
十、总结
- Vue 2.7 同节点上
v-for优先于v-if; - Vue 3.5 同节点上
v-if优先于v-for,不能依赖item/index; - 两个版本都不推荐同节点混用,不要把“能编译”理解成“推荐”;
- 条件与列表无关时,把
v-if放到外层; - 条件依赖单项时,用 computed 过滤;
- 必须逐项判断时,用
<template v-for>; - 过滤不会神奇地消除
O(n)成本,但可以缓存派生列表并减少不必要的渲染; v-if和v-show的选择取决于切换频率与初始化成本;- 使用稳定、唯一的
key,大列表进一步考虑分页和虚拟化。
官方参考
- Vue 2 列表渲染
- Vue 2 条件渲染
- Vue 2.7.16 compiler parser
- Vue 2.7.16 compiler codegen
- Vue 3 列表渲染
- Vue 3 条件渲染
- Vue 3
v-if/v-for迁移 - Vue 3.5.39
transformIf - Vue 3.5.39
transformFor
原文出处
作者:工边页字
来源:稀土掘金
本文保留原文“避免同节点使用 v-if 和 v-for”的主线,并补充 Vue 2.7 与 Vue 3.5 的优先级、性能和迁移说明。