文章

v-if与v-for的优先级问题

v-if与v-for的优先级问题

一句话概括

Vue 2 中 v-for 优先级高于 v-if,两者同处一个元素时每次渲染都会先遍历完整列表再逐项判断条件,浪费性能;Vue 3 反过来,v-if 优先级更高,直接报错阻止这种写法,倒逼开发者用 computed 过滤或 <template> 包裹——本质上是用编译器约束引导最佳实践。

核心知识点

1. 同元素上的优先级机制

Vue 的模板编译器处理同一元素上多个指令时,会按固定优先级排序,高优先级的逻辑包裹低优先级的逻辑,生成嵌套的渲染函数代码。

Vue 2 优先级(部分):v-for > v-if Vue 3 优先级(部分):v-if > v-for

1
2
3
4
5
6
<!-- Vue 2: 先 v-for 后 v-if → 编译为 -->
<li v-for="item in items" v-if="!item.done">{{ item.text }}</li>
<!-- 渲染函数:_l(items, (item) => !item.done ? _c('li', ...) : _e()) -->

<!-- Vue 3: 先 v-if 后 v-for → 编译报错 -->
<!-- Property "item" was accessed during render but is not defined -->

2. Vue 2 的隐藏性能陷阱

Vue 2 先执行 v-for 遍历整个数组,再在每次循环中判断 v-if。假设 10000 项中只显示 1 项,9999 次循环都是无效的——每项都调了渲染函数、创建了 VNode(哪怕是空注释节点),diff 时还要比对。

1
2
3
4
5
6
7
8
// Vue 2 编译结果(伪代码)
function render() {
  return _l(items, function(item) {  // ← 先遍历全部 10000 项
    return !item.done                // ← 再过滤
      ? _c('li', [_v(_s(item.text))])
      : _e()
  })
}

3. Vue 3 的正确姿势

方案一:computed 预过滤(⭐ 推荐)

1
2
3
4
5
6
7
8
const items = ref([
  { id: 1, text: '任务A', done: false },
  { id: 2, text: '任务B', done: true },
])

const undoneItems = computed(() =>
  items.value.filter(item => !item.done)
)
1
<li v-for="item in undoneItems" :key="item.id">{{ item.text }}</li>

优势:computed 有缓存,依赖不变不重算;模板只做渲染职责单一;可测试。

方案二:<template> 包裹

1
2
3
<template v-for="item in items" :key="item.id">
  <li v-if="!item.done">{{ item.text }}</li>
</template>

优势:语义清晰(v-for在外层控制遍历,v-if在内层控制渲染),适合条件简单的场景。

4. Vue 2 → Vue 3 迁移 checklist

如果你的老项目里到处是 v-for + v-if 同元素的写法:

  1. 依赖 item 变量的场景(如 v-if="item.active")→ 提取为 computed
  2. 不依赖 item 变量的场景(如 v-if="showList")→ <template> 包裹外层
  3. 切换频繁的隐藏(如筛选面板折叠)→ 用 v-show 替代
  4. 配合 ESLint 的 vue/no-use-v-if-with-v-for 规则扫描全项目

5. 为什么 Vue 不「智能优化」这种写法?

Vue 团队认为编译器不该猜测开发者意图。v-for + v-if 同元素可能是「只遍历满足条件的项」,也可能是「遍历全部但条件性渲染」。如果编译器自作主张提取 v-if,一旦 v-if 依赖了 item,逻辑就错了。与其搞不确定的「魔法」,不如直接报错让开发者显式决策。

「其实你每天都在用」

  1. 搜索 + 列表:搜 「vue」 显示含 vue 的条目,背后就是 computed 过滤列表 + v-for 渲染,你不会在模板里同时写 search filter 和 for。
  2. 权限过滤:管理后台里不同角色看到不同菜单项,每个请求到达前就已把用户可访问的菜单算好(computed),模板只管渲染。
  3. tab 切换列表:切换 tab A/B/C 时改变过滤条件,computed 缓存确保不切 tab 不重算,比 v-if + v-for 高效一个量级。
  4. 筛选 + 排序 + 分页链式 computedfiltered → sorted → paginated,每步独立缓存,中间某步不变化就不重算下游。
  5. 表格列显隐:用 v-for 遍历所有列配置,v-if 判断 column.visible——这正是 <template v-for> 包裹 v-if 的经典场景。

常见误解(FAQ)

❌ 误区:Vue 3 里 v-if 和 v-for 绝对不能同时存在。 它们在同一个元素上不能共存,但在嵌套层级中完全可以:<template v-for="..." :key="..."><div v-if="..."></div></template>。前者控制遍历,后者在每次遍历中条件渲染,没有指令优先级冲突。

❌ 误区:Vue 2 用 v-for + v-if 过滤列表一点问题都没有。 小列表确实没感觉(3 个 item 遍历 3 次),但列表一大就是灾难。10000 条每帧遍历一次的渲染成本在低端手机上已经会掉帧。这属于「隐式性能陷阱」——代码看起来没毛病,实际上在慢性自杀。

❌ 误区:computed 比 v-if 开销大。 恰恰相反。computed 只在该 ref 变化时重算一次,后续直接用缓存;v-for + v-if 同元素时每次渲染都要遍历+判断。高频更新场景差距极大。

❌ 误区:用 v-show 替代 v-if 就没有优先级问题了。 v-show 确实可以和 v-for 同元素(在 Vue 3 中 v-for 优先级高于 v-show),但它只在 DOM 保留前提下切换 display,大列表初始渲染时 DOM 节点全部存在,内存和首次渲染开销都很大。

一句话总结

v-if + v-for 的优先级反转不是 Vue 在「没事找事」,而是用编译器约束从根源消灭了一个大面积存在的隐式性能陷阱——好的框架不迁就坏的写法。

本文由作者按照 CC BY 4.0 进行授权