技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTVUE

Vue 性能优化:为什么不建议把 v-if 和 v-for 写在同一个元素上?

所属馆藏
Vue
文件格式
Markdown
原始路径
Vue/45-Vue性能优化篇之为什么v-if和v-for不能同时使用?
本文目录12 个章节
  1. 一、同一个元素上同时使用的问题
  2. 二、Vue 2 和 Vue 3 的优先级差异
  3. 三、正确写法一:条件与列表无关时,把 v-if 放到外层
  4. 四、正确写法二:条件依赖每一项时,用 computed 过滤
  5. 五、正确写法三:使用 <template v-for>
  6. 六、v-if 与 v-show 的取舍
  7. 七、key 的正确选择
  8. 八、大列表的进一步优化
  9. 九、面试式回答
  10. 十、总结
  11. 官方参考
  12. 原文出处

Vue 性能优化:为什么不建议把 v-ifv-for 写在同一个元素上?

这篇文章的核心建议仍然成立:不要在同一个元素上同时使用 v-ifv-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>

v-if 与 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 可以访问 itemindex

<!-- 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 === 109 === 10,条件写错了:索引从 0 开始,正确的最后一次判断应是 9 === 9。实际项目不要手工展开模板,使用 computed 过滤更清晰。

Vue 3.5:v-if 优先

Vue 3 中,同一个元素上的 v-if 优先于 v-for。因此以下代码中的 itemindexv-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.7Vue 3.5
同节点指令优先级v-for 高于 v-ifv-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">

应改为已声明的 showTodoscanView 等变量。

四、正确写法二:条件依赖每一项时,用 computed 过滤

如果条件依赖 itemindex,先得到过滤后的数组:

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-ifv-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-ifv-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-ifv-show 的选择取决于切换频率与初始化成本;
  • 使用稳定、唯一的 key,大列表进一步考虑分页和虚拟化。

官方参考

原文出处

作者:工边页字

来源:稀土掘金

本文保留原文“避免同节点使用 v-ifv-for”的主线,并补充 Vue 2.7 与 Vue 3.5 的优先级、性能和迁移说明。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS