孟凡的博客blog.mengfanyaoapp.com
课程资料库›Vue 3 + Element Plus 前端开发›第5次 · 条件渲染与列表渲染:v-if 与 v-for
模块总览

第5次 · 条件渲染与列表渲染:v-if 与 v-for

2026-09-11 · Vue 3 + Element Plus 前端开发 · 课程笔记
课堂练习资料 05-条件渲染与列表渲染v-if与v-for.zip
下载 ZIP 压缩包

第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>

三条规则必须记住:

  1. v-if、v-else-if、v-else 必须紧挨着,中间不能插入别的元素;
  2. v-else-if 可以有多个,v-else 只能有一个且放在最后;
  3. 这一组判断从上往下走,命中一个就停,后面不再判断。

条件里写的是 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>

预期看见什么

  1. 顶部一排筛选按钮(全部 / 已提交 / 未提交)和一个班级下拉框。
  2. 下面按 3 列排布学生卡片,每张卡片有姓名、班级、一个状态标签。
  3. “已提交”的卡片多两行:得分标签和提交时间;“未提交”的卡片只有一句提示文字。
  4. 点卡片上的“切换状态”,标签在这两种之间来回变,同时多出来的两行内容会跟着出现或消失——这就是 v-if 在管这段内容。
  5. 筛选切成“未提交”,只剩 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页 · 换成自己的代码

你的任务

  1. 换成自己的数据:把 students 改成 6 条你自己的“作业清单”(字段:id、name(作业名)、done、score、submitAt)。
  2. 加一个排序:用 computed 算出 sortedList,按成绩从高到低排;再加一个 el-radio-group 在“原顺序 / 按成绩排”之间切换。
  3. 加一个搜索框:el-input 输入关键字,只显示名字里包含关键字的记录(提示:name.includes(keyword))。
  4. 三种空状态文案:没有数据时显示“暂无记录”;有数据但筛选后为空时显示“没有符合筛选条件的记录”。两种文案要不同。
  5. 不要用 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 的区别,以及什么时候用哪个。

在本地写练习记录

  1. 抄下第 4 步的“key 串位”实验现象:改之前错在哪、改之后正常了。
  2. 写下你的三种筛选条件分别过滤了什么。
  3. 记录一次空状态出现的操作过程。

最后交什么

提交 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 别摞一起,先过滤再循环。

不要用这些办法“修复”

  1. 不要为了绕过“v-if 和 v-for 不能一起用”而把 v-if 写到 v-for 的更外层元素上乱猜——正确的是先算数据。
  2. 不要为了消除 key 警告而随手写 :key="Math.random()"。每次渲染 key 都变,等于告诉 Vue“全是新元素”,会导致整段重绘、输入框失焦。
  3. 不要用 v-if 去控制“这段数据有没有”,该用 computed + 空数组判断。

需要老师帮忙时交这些

出问题的那段模板代码、Console 里的完整警告或报错原文、你的数据长什么样、你期望显示几条实际显示几条。

老师巡视时只问三句话

  1. 你这个列表的 key 用的是什么字段?为什么?(能说出唯一稳定)
  2. 空状态是怎么判断出来的?(能指出 length === 0 的那个 v-if)
  3. 这里为什么用 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 必须唯一稳定,否则输入内容会串行。

一分钟现场演示指南

  1. 展示条件渲染:切换“已提交/未提交”,指出同一位置只有一个标签存在。
  2. 展示列表渲染:把数据从 5 条改成 3 条,说明“页面没改一行,条数自己变了”。
  3. 演示 key 的作用:现场把 :key 换成 index,插入一条,指出错位现象,再改回来。
  4. 收尾一句:“条件渲染决定显示不显示,列表渲染决定显示几遍,两者都靠数据说话。”

交作业前最后检查

  • 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 官方文档