在 Vue 开发中,v-for(列表渲染)和v-if(条件渲染)是最常用的两个指令,但当它们出现在同一个元素上时,很多开发者会踩坑 —— 核心问题就是优先级导致的逻辑异常和性能损耗。本文将从优先级原理、错误案例、正确写法、性能优化四个维度,帮你彻底搞懂二者的使用规则,写出符合 Vue 最佳实践的代码。
一、核心结论:v-for 优先级高于 v-if
首先明确 Vue 官方的核心规则(Vue2/Vue3 一致):
当
v-for和v-if作用于同一个元素时,v-for的优先级更高。
这意味着:Vue 会先执行 v-for 循环,再对每个循环项执行 v-if 判断。这个执行顺序看似简单,却会引发两个核心问题:
- 逻辑错误:循环变量(如
item)可能在 v-if 中未定义就被使用; - 性能浪费:即使只需要渲染部分数据,也会先循环所有数据,再过滤,造成不必要的计算。
1. Vue2 vs Vue3 优先级对比
| Vue 版本 | v-for vs v-if 优先级 | 核心差异 |
|---|---|---|
| Vue2 | v-for > v-if | 无警告,直接执行 |
| Vue3 | v-for > v-if | 控制台抛出警告,提示不建议在同一元素使用 |
Vue3 主动抛出警告,正是因为官方不推荐这种写法 —— 既不优雅,也不高效。
二、错误案例:同一元素使用 v-for 与 v-if
1. 案例 1:性能浪费的场景
需求:渲染数组中 “已完成” 的任务列表。
vue
<template> <!-- ❌ 错误写法:先循环所有1000条数据,再判断isComplete --> <div v-for="item in taskList" v-if="item.isComplete" :key="item.id"> {{ item.name }} </div> </template> <script> export default { data() { return { // 模拟1000条任务数据,仅10条已完成 taskList: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `任务${i+1}`, isComplete: i < 10 // 仅前10条完成 })) }; } }; </script>问题分析:
- Vue 会先循环
taskList的 1000 条数据,生成 1000 个虚拟 DOM 节点; - 再对每个节点执行
v-if判断,过滤掉 990 条未完成的任务; - 最终只渲染 10 条,但前面的 990 次循环和判断完全是性能浪费。
2. 案例 2:逻辑错误的场景
需求:循环列表时,根据父组件的showList控制是否渲染整个列表。
vue
<template> <!-- ❌ 错误写法:v-for优先级更高,item未定义时报错 --> <div v-for="item in list" v-if="showList" :key="item.id"> {{ item.name }} </div> </template> <script> export default { data() { return { showList: false, list: [] // 初始为空数组 }; } }; </script>问题分析:
- 由于
v-for优先级更高,即使showList为false,Vue 也会先尝试循环list; - 若
list初始为undefined/null,会直接抛出 “Cannot read property 'length' of undefined” 错误; - 开发者本意是 “如果
showList为 false 就不循环”,但实际执行逻辑完全相反。
三、正确写法:分离 v-for 与 v-if
针对不同场景,有两种核心解决方案,核心思路是让 v-if 的作用范围高于 v-for,避免先循环后过滤。
方案 1:用计算属性过滤数据(推荐)
适用于 “需要过滤循环数据” 的场景(如案例 1),核心是先过滤数据,再循环渲染。
vue
<template> <!-- ✅ 正确写法:只循环过滤后的10条数据 --> <div v-for="item in completedTasks" :key="item.id"> {{ item.name }} </div> </template> <script> export default { data() { return { taskList: Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `任务${i+1}`, isComplete: i < 10 })) }; }, computed: { // 计算属性:提前过滤出已完成的任务 completedTasks() { return this.taskList.filter(item => item.isComplete); } } }; </script>优势:
- 计算属性会缓存结果,只有
taskList变化时才重新计算; - 循环前已过滤数据,避免无效的循环和判断;
- 代码逻辑清晰,便于维护(过滤逻辑集中在计算属性)。
方案 2:在外层元素加 v-if(控制整个列表显示 / 隐藏)
适用于 “控制整个列表是否渲染” 的场景(如案例 2),核心是先判断是否显示,再执行循环。
vue
<template> <!-- ✅ 正确写法:先判断showList,再循环 --> <div v-if="showList"> <div v-for="item in list" :key="item.id"> {{ item.name }} </div> </div> </template> <script> export default { data() { return { showList: false, list: [] }; } }; </script>补充说明:
- 若不想新增外层 DOM 元素,可使用 Vue 的
<template>标签(不会渲染为真实 DOM):vue
<template v-if="showList"> <div v-for="item in list" :key="item.id"> {{ item.name }} </div> </template>
方案 3:特殊场景:循环项内的局部判断
若确实需要对单个循环项做条件判断(而非过滤整个列表),可将 v-if 写在循环项的子元素上:
vue
<template> <div v-for="item in taskList" :key="item.id"> <!-- ✅ 对单个循环项的局部内容做条件判断 --> <span v-if="item.isUrgent" class="urgent">【紧急】</span> {{ item.name }} </div> </template>这种写法不会触发优先级问题,因为v-if作用于循环项的子元素,而非循环元素本身。
四、性能优化:进阶技巧
1. 避免循环中使用方法(替代计算属性)
错误写法:在 v-for 中直接调用方法过滤数据(每次渲染都会重新执行):
vue
<!-- ❌ 性能差:每次渲染都会执行filterTasks() --> <div v-for="item in filterTasks()" :key="item.id"> {{ item.name }} </div>正确写法:用计算属性替代方法,利用缓存减少重复计算。
2. 给 v-for 添加唯一 key(必做)
key是 Vue 跟踪列表项身份的核心,避免使用index作为 key(尤其是列表有增删改查时),否则会导致 DOM 复用异常:
vue
<!-- ✅ 推荐:使用唯一ID作为key --> <div v-for="item in list" :key="item.id">...</div> <!-- ❌ 不推荐:index会随列表变化而变化 --> <div v-for="(item, index) in list" :key="index">...</div>3. 大数据列表:虚拟列表
若循环数据量极大(如 10000 条 +),即使过滤后仍有大量数据,需使用虚拟列表(只渲染可视区域的 DOM):
- Vue2:使用
vue-virtual-scroller; - Vue3:使用
vue-virtual-list或官方的vueuse中的useVirtualList。
五、Vue3 额外注意点:setup 语法糖
在 Vue3 的<script setup>中,逻辑与 Vue2 一致,但写法更简洁,以下是完整示例:
vue
<template> <template v-if="showList"> <div v-for="item in completedTasks" :key="item.id"> {{ item.name }} <span v-if="item.isUrgent" class="urgent">紧急</span> </div> </template> </template> <script setup> import { ref, computed } from 'vue'; // 响应式数据 const showList = ref(true); const taskList = ref( Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `任务${i+1}`, isComplete: i < 10, isUrgent: i < 5 })) ); // 计算属性过滤数据 const completedTasks = computed(() => { return taskList.value.filter(item => item.isComplete); }); </script>六、总结
- 核心规则:
v-for优先级高于v-if,同一元素使用会导致先循环后过滤,引发性能 / 逻辑问题(Vue3 会抛出警告)。 - 正确写法:
- 过滤列表数据:用计算属性提前过滤,再循环;
- 控制列表显示:在外层(
<template>/ 普通元素)加v-if,再循环; - 单个项局部判断:将
v-if写在循环项的子元素上。
- 性能优化:计算属性缓存、唯一 key、大数据用虚拟列表,避免循环中调用方法。
遵循这些规则,既能避免 90% 的 v-for/v-if 相关 bug,又能保证列表渲染的性能,这也是 Vue 官方推荐的最佳实践。