第5次 · 条件渲染与列表渲染:v-if 与 v-for
第5次 · 条件渲染与列表渲染:v-if 与 v-for
第1/7页 · 先知道今天要做什么
前面四课,页面上显示的内容都是写死的。真实项目里,显示什么、显示多少条,取决于数据:有数据就渲染一片卡片,没数据就显示“暂无内容”;状态是“已提交”就显示绿标签,是“未提交”就显示灰标签。决定“显不显示”的叫条件渲染(v-if),决定“重复显示多少遍”的叫列表渲染(v-for)。 这两个指令是 Vue 模板里出现频率最高的两个,今天一次讲透。
今天的场景
一个“作业提交情况”面板。后端给回来一个数组,里面有每位学生和他们的提交状态。你要做四件事:按状态筛选、把筛选结果循环渲染成卡片、没有结果时显示空状态、给不同状态上不同颜色的标签。
下课时拿出这三样结果
| 做到什么 | 在哪里看 | 完成的证据 |
|---|---|---|
会用 v-if / v-else / v-else-if |
页面上的标签 | 同一张卡片里,“已提交”显示绿色标签,“未提交”显示灰色标签,且两者互斥只出现一个。 |
会用 v-for 渲染列表 |
卡片区域 | 数据有几条就渲染几张卡片,加一条数据页面自动多一张。 |
知道 key 是干什么的 |
页面 + Console | 能说出为什么 :key 要写唯一值,删掉中间一条为什么不会串位。 |
你只需要带着这些基础来
会写 ref 与 .value;会用 computed 做筛选;第 4 课的工程还能跑。
本课时 45 分钟怎么上
| 时间 | 老师带着做 | 学生留下什么 |
|---|---|---|
| 0—5 分钟 | 提问:如果数组里有 50 个人,难道要写 50 个 div? | 明白“模板要能跟着数据长出来”。 |
| 5—18 分钟 | 讲 v-if / v-else-if / v-else / v-show,讲清楚两者的区别与选择标准。 |
笔记里写下 v-if 与 v-show 的使用分界。 |
| 18—32 分钟 | 讲 v-for:数组、对象、数字;讲 :key;讲为什么不要和 v-if 写在同一层。 |
能判断一处代码该不该拆开。 |
| 32—42 分钟 | 粘贴本课 App.vue,切换筛选、改状态、观察空状态出现与消失。 |
四项功能全部跑通。 |
| 42—45 分钟 | 抽查:问“删掉 :key 会怎样”。 |
全员提交。 |
从哪里开始
复制上一课的工程改名 lesson-vue-05。不需要新增依赖。
今天先掌握到这里
今天只讲“显示什么、显示几遍”。事件处理与 v-model 的细节在第 6 课,组件拆分在第 7 课。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第2/7页 · 把新词说清楚
新词与本课含义
| 名字 | 用熟悉的话理解 | 本课里长什么样 |
|---|---|---|
| 条件渲染 | 满足条件才把这段 HTML 放进页面。 | v-if="status === 'done'" |
| 列表渲染 | 一段 HTML 按数组重复生成多份。 | v-for="item in list" |
| 指令 | 写在标签上的、以 v- 开头的特殊属性,Vue 会读懂它。 |
v-if、v-for、v-bind、v-on |
:key |
给这一批重复元素里每一个贴的唯一编号,帮 Vue 认出“谁是谁”。 | :key="item.id" |
| 空状态 | 一个结果都没有时显示的提示,而不是空白一片。 | <el-empty> |
v-if 家族:显不显示
<div v-if="score >= 90">优秀</div>
<div v-else-if="score >= 60">及格</div>
<div v-else>不及格</div>
三条规则必须记住:
v-if、v-else-if、v-else必须紧挨着,中间不能插入别的元素;v-else-if可以有多个,v-else只能有一个且放在最后;- 这一组判断从上往下走,命中一个就停,后面不再判断。
条件里写的是 JavaScript 表达式,不是字符串。所以 v-if="status === 'done'" 里的两个等号是“比较”,三个等号是“严格比较”,都不能漏。
v-if 和 v-show 怎么选
v-if |
v-show |
|
|---|---|---|
| 做法 | 条件为假时,元素从页面里删掉 | 元素一直在,只是被加上 display: none |
| 第一次渲染代价 | 大(不满足就不创建) | 小(都创建,只是藏起来) |
| 切换代价 | 大(要创建或销毁) | 小(只改一个样式) |
| 适合场景 | 条件很少变(例如“是否登录”) | 条件频繁切换(例如“展开/收起”“标签页”) |
一句话记法:很少变的用 v-if,频繁切的用 v-show。
v-for 三种写法
<!-- 1. 数组:最常用 -->
<li v-for="item in students" :key="item.id">{{ item.name }}</li>
<!-- 2. 带下标 -->
<li v-for="(item, index) in students" :key="item.id">{{ index + 1 }}. {{ item.name }}</li>
<!-- 3. 对象:按“值、键、下标”取 -->
<div v-for="(value, key, index) in info" :key="key">{{ key }} = {{ value }}</div>
:key 到底解决什么问题
Vue 更新列表时,为了快,会尽量复用已有的 DOM 元素,只改里面的文字。没有 key 时它只能按“第几个”来猜对应关系。于是就会出现你熟悉的那类怪事:在列表中间插入一条,下面的输入框里残留着上一行的内容。
key 就是告诉 Vue:“这一条的身份是 id=3,别拿它去顶别人的位置。”
| 写法 | 后果 |
|---|---|
:key="item.id"(唯一且稳定) |
正确。增删改都不串位。 |
:key="index" |
能跑,但列表中间插入/删除时会串位,输入框内容会错行。 |
不写 :key |
Vue 会警告,行为等同于用 index,同样会串位。 |
结论:只要数据里有 id 之类的唯一字段,就一律用它当 key。
v-for 和 v-if 不要写在同一层
<!-- 不推荐:Vue 3 里 v-if 的优先级高于 v-for,会报"找不到变量" -->
<li v-for="item in list" v-if="item.done" :key="item.id">...</li>
<!-- 推荐:先算好要显示的数据,再循环 -->
<li v-for="item in doneList" :key="item.id">...</li>
正确做法是在 computed 里过滤好(第 4 课学的就是干这个用的),模板里只负责循环。
只看这几行:筛选 + 循环的骨架
<script setup>
import { ref, computed } from 'vue'
const filter = ref('all') // 当前筛选条件
const students = ref([ /* ...后端给的数据... */ ])
// 算出“真正要显示的数组”,模板只管循环它
const visibleList = computed(() => {
if (filter.value === 'done') return students.value.filter(s => s.done)
if (filter.value === 'todo') return students.value.filter(s => !s.done)
return students.value
})
</script>
<template>
<p v-if="visibleList.length === 0">暂无记录</p>
<div v-for="item in visibleList" :key="item.id">{{ item.name }}</div>
</template>
先口头预测:如果筛选条件切成“未提交”,而恰好所有人都提交了,页面上会出现什么?
出现“暂无记录”那段。 因为 visibleList 是空数组,v-if="visibleList.length === 0" 为真。这就是条件渲染 + 列表渲染配合的典型写法。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第3/7页 · 先看一个小例子
完整代码:App.vue(作业提交情况面板)
<script setup>
import { ref, computed } from 'vue'
import { ElCard, ElRadioGroup, ElRadioButton, ElSelect, ElOption, ElTag, ElEmpty, ElAlert, ElButton, ElMessage } from 'element-plus'
import 'element-plus/dist/index.css'
const students = ref([
{ id: 1, name: '张三', cls: '计算机1班', done: true, score: 92, submitAt: '09-01 20:15' },
{ id: 2, name: '李四', cls: '计算机1班', done: false, score: null, submitAt: '' },
{ id: 3, name: '王五', cls: '计算机2班', done: true, score: 78, submitAt: '09-02 10:03' },
{ id: 4, name: '赵六', cls: '计算机2班', done: true, score: 45, submitAt: '09-02 22:40' },
{ id: 5, name: '钱七', cls: '计算机3班', done: false, score: null, submitAt: '' }
])
const filter = ref('all') // all | done | todo
const cls = ref('all') // all | 班级名
const visibleList = computed(() => {
let list = students.value
if (filter.value === 'done') list = list.filter(s => s.done)
if (filter.value === 'todo') list = list.filter(s => !s.done)
if (cls.value !== 'all') list = list.filter(s => s.cls === cls.value)
return list
})
const stat = computed(() => {
const done = students.value.filter(s => s.done).length
return { total: students.value.length, done, todo: students.value.length - done }
})
function toggleDone(id) {
const s = students.value.find(x => x.id === id)
if (s) s.done = !s.done
}
function gradeTag(score) {
if (score === null) return 'info'
if (score >= 85) return 'success'
if (score >= 60) return 'warning'
return 'danger'
}
</script>
<template>
<div class="page">
<h1 class="title">作业提交情况</h1>
<el-card class="block">
<div class="bar">
<el-radio-group v-model="filter">
<el-radio-button label="all" value="all">全部</el-radio-button>
<el-radio-button label="done" value="done">已提交</el-radio-button>
<el-radio-button label="todo" value="todo">未提交</el-radio-button>
</el-radio-group>
<el-select v-model="cls" style="width: 160px">
<el-option label="全部班级" value="all" />
<el-option label="计算机1班" value="计算机1班" />
<el-option label="计算机2班" value="计算机2班" />
<el-option label="计算机3班" value="计算机3班" />
</el-select>
</div>
<p class="stat">
共 {{ stat.total }} 人,已提交 {{ stat.done }} 人,未提交 {{ stat.todo }} 人
</p>
</el-card>
<el-empty v-if="visibleList.length === 0" description="没有符合条件的记录" />
<div v-else class="grid">
<el-card v-for="item in visibleList" :key="item.id" class="stu" shadow="hover">
<div class="row">
<span class="name">{{ item.name }}</span>
<el-tag v-if="item.done" type="success" size="small">已提交</el-tag>
<el-tag v-else type="info" size="small">未提交</el-tag>
</div>
<p class="cls">{{ item.cls }}</p>
<template v-if="item.done">
<p>得分:<el-tag :type="gradeTag(item.score)" size="small">{{ item.score }}</el-tag></p>
<p class="time">提交于 {{ item.submitAt }}</p>
</template>
<p v-else class="time">尚未提交,请尽快补交</p>
<el-button size="small" @click="toggleDone(item.id)">切换状态</el-button>
</el-card>
</div>
<el-alert
title="切换上方筛选,观察卡片数量;把所有卡片都切成“已提交”,再切到“未提交”看空状态。"
type="info" :closable="false" show-icon class="tip"
/>
</div>
</template>
<style>
html, body, #app {
margin: 0; padding: 0; max-width: none; width: auto; height: auto;
display: block; place-items: initial; text-align: left; background: #f5f7fa;
}
</style>
<style scoped>
.page { max-width: 1000px; margin: 28px auto; padding: 0 16px; }
.title { font-size: 22px; color: #303133; margin: 0 0 18px; }
.block { margin-bottom: 18px; }
.bar { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.stat { margin: 12px 0 0; color: #606266; font-size: 14px; }
.grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
.stu { min-height: 180px; }
.row { display: flex; justify-content: space-between; align-items: center; }
.name { font-weight: 600; color: #303133; }
.cls { color: #909399; font-size: 13px; margin: 6px 0 10px; }
.time { color: #909399; font-size: 12px; }
.tip { margin-top: 18px; }
@media (max-width: 820px) { .grid { grid-template-columns: 1fr; } }
</style>
预期看见什么
- 顶部一排筛选按钮(全部 / 已提交 / 未提交)和一个班级下拉框。
- 下面按 3 列排布学生卡片,每张卡片有姓名、班级、一个状态标签。
- “已提交”的卡片多两行:得分标签和提交时间;“未提交”的卡片只有一句提示文字。
- 点卡片上的“切换状态”,标签在这两种之间来回变,同时多出来的两行内容会跟着出现或消失——这就是
v-if在管这段内容。 - 筛选切成“未提交”,只剩 2 张卡片;把这两张都切成“已提交”,页面立刻变成空状态提示。
先猜一猜,读完代码再回答
模板里有这么两段:
<el-tag v-if="item.done" type="success">已提交</el-tag>
<el-tag v-else type="info">未提交</el-tag>
<template v-if="item.done">
<p>得分:...</p>
<p>提交于 ...</p>
</template>
第二个为什么要用 <template> 包起来?
因为 v-if 只能写在一个元素上。如果直接写在 <p> 上,那这两行就会变成“要么都有、要么都没有”里的一行对一行——实际上它们需要作为一个整体**一起显示或一起隐藏。<template> 是个“空壳”,渲染时不会产生任何多余标签,专门用来把多段内容打包,交给 v-if / v-for 统一控制。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第4/7页 · 一步一步做出来
第1步:最小的 v-if
新建或复用工程,把 App.vue 清成:
<script setup>
import { ref } from 'vue'
const logged = ref(false)
</script>
<template>
<p v-if="logged">欢迎回来</p>
<p v-else>请先登录</p>
<button @click="logged = !logged">切换</button>
</template>
点按钮。预期:两句话互相替换,页面上始终只有一句。
第2步:体验 v-show 的区别
把上面那段改成:
<p v-show="logged">欢迎回来</p>
点按钮。预期:画面看起来一样。按 F12 打开 Elements 面板,再点按钮,预期:<p> 标签一直都在,只是多/少了 style="display: none"。
这一步是理解两者区别最直观的方式:v-if 是“删掉/新建”,v-show 是“藏起来/露出来”。
第3步:最小的 v-for
<script setup>
import { ref } from 'vue'
const names = ref(['张三', '李四', '王五'])
</script>
<template>
<ul>
<li v-for="(n, i) in names" :key="n">{{ i + 1 }}. {{ n }}</li>
</ul>
</template>
预期:三行列表。往 names 里再加一项,页面自动多一行。
第4步:亲眼看一次“key 串位”
先故意写错:用 :key="i"(下标当 key),并在每一项里放一个输入框:
<input v-for="(n, i) in names" :key="i" v-model="names[i]" />
<button @click="names.splice(1, 0, '新同学')">在第二位插入一个人</button>
在三个输入框里分别输入 A、B、C。点“插入”。预期:插进来的新行是空的,但原来的 A / B / C 位置乱了,出现内容错行。
改成 :key="n" 再试一次。预期:每条内容都跟着自己走,不错位。
这就是 key 存在的意义。演示完记得改回 :key="n"。
第5步:整段替换为本课完整版
把第 3 页的完整 App.vue 整段覆盖 src/App.vue。确认:
- 卡片渲染出来了;
- 切换筛选时数量对得上;
- 空状态会按预期出现。
第6步:动手改数据
把第 2 位的李四改成“已提交”,看统计数字和卡片内容一起变化;再删掉数组里的最后一个人(注释掉那一行),看总数从 5 变 4。
本课文件速查
| 位置 | 本次作用 |
|---|---|
filter / cls |
两个筛选条件(原始数据)。 |
visibleList |
computed 算出的“真正要显示的数组”,模板只循环它。 |
模板里的 v-if="visibleList.length === 0" |
空状态的分支。 |
卡片里的 v-if="item.done" / v-else |
同一位置二选一显示。 |
<template v-if="item.done"> |
把两行内容打包,一起显示或隐藏。 |
完成修改后检查
| 操作或输入 | 预期观察 |
|---|---|
| 切到“已提交” | 只剩 3 张卡片,标签全是绿色。 |
| 把 5 个人全切成已提交,再切到“未提交” | 出现“没有符合条件的记录”空状态。 |
| 选“计算机2班” | 只剩 2 张卡片。 |
| 删除数组中间一项 | 卡片不串位(因为用了 :key="item.id")。 |
把 :key 改成 index 再删中间一项 |
能看出错位现象。 |
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第5/7页 · 换成自己的代码
你的任务
- 换成自己的数据:把
students改成 6 条你自己的“作业清单”(字段:id、name(作业名)、done、score、submitAt)。 - 加一个排序:用
computed算出sortedList,按成绩从高到低排;再加一个el-radio-group在“原顺序 / 按成绩排”之间切换。 - 加一个搜索框:
el-input输入关键字,只显示名字里包含关键字的记录(提示:name.includes(keyword))。 - 三种空状态文案:没有数据时显示“暂无记录”;有数据但筛选后为空时显示“没有符合筛选条件的记录”。两种文案要不同。
- 不要用 index 当 key:检查你的列表,确认
:key用的是唯一字段。
按这张操作单完成
| 步骤 | 改哪里 | 运行后应该看到什么 |
|---|---|---|
| 1 换数据 | students 数组 |
6 条自己的记录,统计数字随之变化。 |
| 2 加排序 | 新增 sortedList computed + 一组单选 |
切换后卡片顺序改变。 |
| 3 加搜索 | 新增 keyword ref + el-input + 过滤条件 |
输入“实验”只剩含“实验”的作业。 |
| 4 两种空状态 | 模板里改 v-if 分支 |
两种情况文案不同。 |
| 5 检查 key | 所有 v-for |
都使用唯一字段。 |
最低完成标准:先把这三项做实
| 检查项 | 通过条件 |
|---|---|
| 条件渲染 | 至少用到一次 v-else,并用到一次 <template v-if> 打包多段内容。 |
| 列表渲染 | 所有 v-for 都写了 :key,且值唯一稳定。 |
| 说完为什么 | 能说清 v-if 与 v-show 的区别,以及什么时候用哪个。 |
在本地写练习记录
- 抄下第 4 步的“key 串位”实验现象:改之前错在哪、改之后正常了。
- 写下你的三种筛选条件分别过滤了什么。
- 记录一次空状态出现的操作过程。
最后交什么
提交 src/App.vue 与 练习记录.md。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第6/7页 · 出错时从哪里查
把现象和检查动作对上
| 看到什么现象 / 报错 | 先检查哪里 | 修好后怎样确认 |
|---|---|---|
v-if 和 v-else 中间夹了别的元素,v-else 不生效 |
两者必须紧挨着,中间不能有其他标签(注释也算)。 | 移走中间元素后恢复正常。 |
页面报 Property "xxx" was accessed during render but is not defined |
v-for 和 v-if 写在同一层,v-if 里用了循环变量。 |
改成先 computed 过滤,再循环。 |
Console 警告 Duplicate keys found |
:key 的值有重复。 |
换成真正唯一的字段。 |
| 列表更新后内容错位 | 用了 index 或没写 key。 |
改成唯一字段。 |
v-if 的条件一直为真(或一直为假) |
条件写成了字符串,例如 v-if="done" 想表达“等于已完成”。 |
写成明确的表达式 v-if="item.done"。 |
v-for 写在 <template> 上后样式丢了 |
class 写在 <template> 上不会生效(它不产生标签)。 |
把 class 挪到内部真实元素上。 |
排错口诀
v-if 家族要挨着,命中一个就停下; 很少变的用 v-if,频繁切的用 v-show; v-for 必须带 key,key 要唯一不要用下标; v-for 和 v-if 别摞一起,先过滤再循环。
不要用这些办法“修复”
- 不要为了绕过“v-if 和 v-for 不能一起用”而把
v-if写到v-for的更外层元素上乱猜——正确的是先算数据。 - 不要为了消除
key警告而随手写:key="Math.random()"。每次渲染 key 都变,等于告诉 Vue“全是新元素”,会导致整段重绘、输入框失焦。 - 不要用
v-if去控制“这段数据有没有”,该用computed+ 空数组判断。
需要老师帮忙时交这些
出问题的那段模板代码、Console 里的完整警告或报错原文、你的数据长什么样、你期望显示几条实际显示几条。
老师巡视时只问三句话
- 你这个列表的
key用的是什么字段?为什么?(能说出唯一稳定) - 空状态是怎么判断出来的?(能指出
length === 0的那个v-if) - 这里为什么用
v-show而不是v-if?(能说出切换频率)
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档
第7/7页 · 用自己的话讲一遍
回到第 3 页:你刚才猜对了吗?
为什么 <template v-if> 能一次管两行?
参考解释:<template> 是一个不产生真实元素的容器,作用就是把多段内容打成“一包”,交给指令统一控制。v-if 一次只能挂在一个节点上,所以想“一起显示一起隐藏”就必须先打包。
回答本课的两个问题
| 问题 | 参考解释 |
|---|---|
为什么 key 不能用下标? |
下标只表示“第几个”,删除中间一条后,后面每条的下标都会变。Vue 会误以为“位置 2 的内容变成了原来位置 3 的内容”,从而复用了错误的 DOM,导致输入框内容、动画状态错位。 |
v-if 和 v-for 为什么不能写在一起? |
Vue 3 里 v-if 的优先级更高,会先执行,此时循环变量还不存在,于是报“未定义”。正确做法是在 computed 里先算出要显示的数组。 |
五个必须能答上来的问题
| 考考自己 | 可以这样通俗回答 |
|---|---|
v-if、v-else-if、v-else 的书写要求是什么? |
必须相邻,中间不能插别的元素;v-else 最多一个且在最后。 |
v-show 的本质是什么? |
元素始终存在,只是切换 display。 |
v-for 能遍历哪几种东西? |
数组、对象、数字,也可以遍历字符串。 |
空状态为什么不用 v-if="!list"? |
空数组是“有值但长度为 0”,![] 是 false,判断不出来,要用 list.length === 0。 |
| 列表里放输入框时最该注意什么? | key 必须唯一稳定,否则输入内容会串行。 |
一分钟现场演示指南
- 展示条件渲染:切换“已提交/未提交”,指出同一位置只有一个标签存在。
- 展示列表渲染:把数据从 5 条改成 3 条,说明“页面没改一行,条数自己变了”。
- 演示 key 的作用:现场把
:key换成index,插入一条,指出错位现象,再改回来。 - 收尾一句:“条件渲染决定显示不显示,列表渲染决定显示几遍,两者都靠数据说话。”
交作业前最后检查
src/App.vue与练习记录.md已保存。- 所有
v-for都已写:key,且不是下标。 - Console 里没有
key相关警告。
课后再做一个小对照
把本课页面里的 <el-tag v-if> / v-else 那一块,改成 <el-tag :type="item.done ? 'success' : 'info'"> 加一句三元表达式的写法。两种写法都能达到目的,比较一下:哪种更好读?什么情况下必须用 v-if(提示:当两个分支的长度、结构完全不同时)。
来源:本课自编 Vue 3 前端核心技能实训 · 语法依据 Vue 3.5 官方文档(条件渲染 / 列表渲染章节)· 组件依据 Element Plus 2.8 官方文档