v-if 和 v-for 为什么不建议写在同一个元素上?
Vue 2 里同一个元素上 v-for 优先级更高,每一项都会先渲染再判断;Vue 3 改成 v-if 优先,v-if 拿不到 v-for 的循环变量,直接报错。两个版本都建议过滤列表用 computed,整体显示隐藏把 v-if 放到外层或 template 上。
简答
列表里只想显示已完成的待办,很多人会写
<li v-for="todo in todos" v-if="todo.done">。这种写法在 Vue 2 和 Vue 3 里表现不一样,官方都建议不要这么写。区别在优先级。Vue 2 里同一个元素上
v-for先执行,相当于先循环,再对每一项判断v-if,能跑起来,但每次渲染都要遍历整个列表。Vue 3 改成了v-if先执行,这时循环还没开始,todo还不存在,访问todo.done就会报错。正确写法分两种情况。要过滤列表,就用
computed先算出过滤后的数组,再v-for这个数组,结果有缓存,列表不变时不会重复过滤。要根据条件决定整个列表显不显示,就把v-if放到外层元素上,或者用<template v-for>包一层,把v-if写在里面的元素上。要注意,Vue 2 升级到 Vue 3 时,这种写法不一定会在编译期暴露,常常是页面运行时报
todo未定义,迁移时要专门搜一遍。
为什么 Vue 3 会报错
下面这段模板在 Vue 2 里能正常过滤,在 Vue 3 里会出错:
<li v-for="todo in todos" v-if="!todo.done">
{{ todo.name }}
</li>
Vue 3 文档的说法是:两个指令在同一个节点上时,v-if 的优先级更高,所以 v-if 的条件访问不到 v-for 作用域里的变量。可以理解成 Vue 3 把它编译成了“先判断条件,条件成立再循环”。判断条件时 todo 还没定义,表达式就出错了。
Vue 2 正好反过来,v-for 优先。它相当于“循环每一项,每一项再判断”,所以能拿到 todo。可是哪怕最后只显示一两项,每次重新渲染时整个列表都要遍历一遍。
过滤列表用 computed
把过滤逻辑挪到 computed 里:
<script setup>
import { computed, ref } from 'vue';
const todos = ref([
{ id: 1, name: '写周报', done: false },
{ id: 2, name: '交房租', done: true },
]);
const activeTodos = computed(() => todos.value.filter((todo) => !todo.done));
</script>
<template>
<ul>
<li v-for="todo in activeTodos" :key="todo.id">{{ todo.name }}</li>
</ul>
</template>
这样模板里只剩一个 v-for,没有优先级问题。computed 会缓存结果,只有 todos 变了才重新过滤;页面上其他数据变化引起重新渲染时,直接用上次的结果。
只想整体显示或隐藏时怎么写
如果条件跟每一项无关,比如“列表为空时不显示”,就把 v-if 放到外层:
<ul v-if="todos.length > 0">
<li v-for="todo in todos" :key="todo.id">{{ todo.name }}</li>
</ul>
如果确实要在循环里逐项判断,又不想多一层 DOM,可以用 <template> 承载 v-for:
<template v-for="todo in todos" :key="todo.id">
<li v-if="!todo.done">{{ todo.name }}</li>
</template>
<template> 不会渲染成真实元素,v-for 在外面、v-if 在里面,先后关系一目了然,两个版本行为一致。
常见追问
v-for 为什么要写 key?
key 让 Vue 在列表更新时认出哪一项是哪一项,从而复用对应的 DOM 和组件状态。不写或用下标时,插入、删除、排序会让状态错位,原理见《Vue 2 的 diff 算法是怎样比较子节点的?》。
v-if 和 v-show 怎么选?
v-if条件为假时元素不渲染,切换时会创建和销毁;v-show始终渲染,只切换display。频繁切换用v-show,很少改变的条件用v-if。详见《v-if 和 v-show 应该怎样选择?》。