Vue.js: 条件渲染与列表渲染
最后更新:2026-08-26
条件渲染(v-if / v-show)和列表渲染(v-for)是 Vue 模板最常用的两个指令。前者控制"显示不显示",后者控制"显示多少个"。两者组合能渲染 90% 的 UI 场景。
掌握 v-if 和 v-for 的 5 大区别,以及 :key 的正确用法,是写出高性能 Vue 应用的关键。
1. 你将学到
v-if/v-else-if/v-else条件渲染v-showvsv-if的 5 大区别v-for遍历数组、对象、数字、字符串:key的作用和最佳实践template元素包装多个元素- 列表过滤、排序、分页
- 虚拟滚动优化(10000+ 项)
2. 一个仪表盘"if 套娃"的代码灾难
(1) 痛点:5 个 v-if 嵌套,调试地狱
Alice 的仪表盘代码失控了:
VUE
<!-- ❌ 翻车版:v-if 嵌套地狱 -->
<template>
<div v-if="user">
<div v-if="user.isAdmin">
<div v-if="orders.length > 0">
<div v-if="!isLoading">
<div v-if="!hasError">
<!-- 终于显示内容 -->
<p>Orders: {{ orders.length }}</p>
</div>
<div v-else>Error: {{ errorMsg }}</div>
</div>
<div v-else>Loading...</div>
</div>
<div v-else>No orders</div>
</div>
<div v-else>Not admin</div>
</div>
<div v-else>Please login</div>
</template>
Charlie 的代码审查意见:
"Alice,这根本没法读。5 层 v-if 嵌套。再加 2 个条件怎么办?我们需要更清晰的模式。"
(2) Vue 解法:v-if 扁平化 + 计算属性
VUE
<template>
<div v-if="!user">Please login</div>
<div v-else-if="!user.isAdmin">Not admin</div>
<div v-else-if="isLoading">Loading...</div>
<div v-else-if="hasError">Error: {{ errorMsg }}</div>
<div v-else-if="orders.length === 0">No orders</div>
<div v-else>
<p>Orders: {{ orders.length }}</p>
<OrderList :orders="orders" />
</div>
</template>
<script setup>
const { computed } = Vue
const props = defineProps(['user', 'orders', 'isLoading', 'hasError', 'errorMsg'])
// 把 5 个条件扁平化(v-else-if 链)
</script>
(3) 收益
- 可读性:5 层嵌套 → 6 行平铺
- 可维护:加条件只需加一行 v-else-if
- 调试:每行独立条件,便于打断点
3. v-if / v-else-if / v-else 条件渲染
(1) 基本语法
VUE
<template>
<!-- 单条件 -->
<p v-if="isVisible">Visible</p>
<!-- 二选一 -->
<p v-if="score >= 60">Passed</p>
<p v-else>Failed</p>
<!-- 多选一 -->
<p v-if="type === 'A'">Type A</p>
<p v-else-if="type === 'B'">Type B</p>
<p v-else-if="type === 'C'">Type C</p>
<p v-else>Unknown</p>
</template>
(2) template 元素
VUE
<template>
<!-- template:包装多个元素,不渲染 DOM -->
<template v-if="isAdmin">
<h1>Admin Dashboard</h1>
<p>Welcome, {{ user.name }}</p>
<button>Edit</button>
</template>
<!-- 不会渲染 template 元素(节省 1 层 DOM)-->
</template>
(3) 5 大使用场景
| 场景 | 用 v-if |
|---|---|
| 登录/未登录 | ✅ |
| 加载中/加载完成 | ✅ |
| 空数据/有数据 | ✅ |
| 错误状态 | ✅ |
| 频繁切换显示/隐藏 | ❌(用 v-show) |
4. v-show vs v-if 5 大区别
(1) 核心区别
VUE
<template>
<!-- v-if:条件为 false 时,元素从 DOM 移除 -->
<p v-if="show">v-if</p>
<!-- show=false: 元素不存在于 DOM -->
<!-- v-show:条件为 false 时,display: none -->
<p v-show="show">v-show</p>
<!-- show=false: <p style="display: none">v-show</p> -->
</template>
(2) 5 大对比
| 维度 | v-if | v-show |
|---|---|---|
| DOM 操作 | 条件 false 时移除元素 | 始终在 DOM,只切 display |
| 切换性能 | 差(每次销毁/重建) | 好(只切 CSS) |
| 初始渲染 | 条件 false 时不渲染 | 始终渲染(即使不可见) |
| 配合 transition | ✅ 完美(动画进出) | ❌ 不支持 |
| 适用场景 | 偶尔切换 | 频繁切换 |
(3) 选择建议
JS
// 偶尔切换(如登录/未登录)→ v-if
<div v-if="isLoggedIn">Welcome, {{ user.name }}</div>
<div v-else>Please login</div>
// 频繁切换(如 tab 切换)→ v-show
<button v-show="isActive">Active</button>
<button v-show="!isActive">Inactive</button>
5. v-for 列表渲染
(1) 遍历数组
VUE
<template>
<!-- 基础遍历 -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 带索引 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
</ul>
</template>
<script setup>
const { ref } = Vue
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
</script>
(2) 遍历对象
VUE
<template>
<ul>
<li v-for="(value, key, index) in user" :key="key">
{{ index + 1 }}. {{ key }}: {{ value }}
</li>
</ul>
</template>
<script setup>
const { reactive } = Vue
const user = reactive({
name: 'Alice',
age: 25,
email: 'alice@example.com'
})
// 输出:1. name: Alice 2. age: 25 3. email: alice@example.com
</script>
(3) 遍历数字
VUE
<template>
<!-- 渲染 1 到 10 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
<!-- 输出:12345678910 -->
</template>
(4) 遍历字符串
VUE
<template>
<!-- 每个字符一个 span -->
<span v-for="char in 'Hello'" :key="char">{{ char }}</span>
<!-- 输出:H e l l o -->
</template>
6. :key 的作用和最佳实践
(1) :key 是什么?
:key 给每个 v-for 项一个唯一标识,帮助 Vue 高效更新 DOM。
VUE
<template>
<!-- ❌ 没有 :key(Vue 默认用 index) -->
<li v-for="item in items">{{ item.name }}</li>
<!-- ✅ 有 :key(用唯一 id) -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</template>
(2) 为什么需要 :key?
graph LR
A[原列表 A B C] --> B[新列表 B C D]
B --> C[没有 key:可能错位]
B --> D[有 key:正确更新]
style C fill:#ff6b6b
style D fill:#42b883
没有 :key 时:
- Vue 用 index 比较
- 删除 A 后,B/C 变成 (0)/(1),但 Vue 以为是 "B/C 还在"
- 导致组件状态错位
有 :key 时:
- Vue 用 id 比较
- 精确识别哪些新增/删除/移动
- 性能最优
(3) :key 最佳实践
VUE
<!-- ✅ 推荐:唯一 ID -->
<li v-for="user in users" :key="user.id">
<!-- ✅ 次选:唯一字段(邮箱、手机号等)-->
<li v-for="user in users" :key="user.email">
<!-- ❌ 避免:index(数据变化时会错位)-->
<li v-for="(user, index) in users" :key="index">
<!-- ❌ 绝对不要:random()(每次渲染都不同,强制重建)-->
<li v-for="user in users" :key="Math.random()">
7. 列表过滤 / 排序 / 分页
(1) 过滤 + 排序(计算属性)
JS
const { ref, computed } = Vue
const items = ref([
{ id: 1, name: 'iPhone', price: 999, category: 'phone' },
{ id: 2, name: 'MacBook', price: 2499, category: 'laptop' },
{ id: 3, name: 'iPad', price: 599, category: 'tablet' }
])
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('price') // 'price' / 'name'
// 过滤 + 排序(computed 自动缓存)
const filteredItems = computed(() => {
let result = items.value
// 1. 按搜索词过滤
if (searchQuery.value) {
result = result.filter(item =>
item.name.toLowerCase().includes(searchQuery.value.toLowerCase())
)
}
// 2. 按分类过滤
if (selectedCategory.value !== 'all') {
result = result.filter(item => item.category === selectedCategory.value)
}
// 3. 排序
result = [...result].sort((a, b) => {
if (sortBy.value === 'price') return a.price - b.price
if (sortBy.value === 'name') return a.name.localeCompare(b.name)
return 0
})
return result
})
(2) 分页
JS
const currentPage = ref(1)
const pageSize = ref(10)
// 分页后的数据
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return filteredItems.value.slice(start, start + pageSize.value)
})
// 总页数
const totalPages = computed(() =>
Math.ceil(filteredItems.value.length / pageSize.value)
)
8. 完整示例:动态商品列表
▶ 示例:5 种 v-if/v-show 写法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<!-- 1. v-if:条件渲染(不在 DOM) -->
<p v-if="show">1. v-if 显示(点击切换)</p>
<!-- 2. v-show:display none(始终在 DOM) -->
<p v-show="show">2. v-show 显示</p>
<!-- 3. v-else-if:链式条件 -->
<p v-if="type === 'A'">3. 类型 A</p>
<p v-else-if="type === 'B'">3. 类型 B</p>
<p v-else>3. 其他类型</p>
<!-- 4. v-if + template:多元素 -->
<template v-if="isAdmin">
<h1>4. Admin Panel</h1>
<button>Edit</button>
</template>
<!-- 5. v-if vs v-show 性能对比 -->
<div v-if="occasionally">5a. 偶尔切换 (v-if)</div>
<div v-show="frequently">5b. 频繁切换 (v-show)</div>
<p><button @click="show = !show">切换 show</button></p>
<p><button @click="type = type === 'A' ? 'B' : 'A'">切换 type</button></p>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const show = ref(true)
const type = ref('A')
const isAdmin = ref(true)
const occasionally = ref(false)
const frequently = ref(true)
return { show, type, isAdmin, occasionally, frequently }
}
}).mount('#app')
</script>
▶ 示例:5 种 v-for 写法
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<h4>1. 数组</h4>
<ul><li v-for="item in items" :key="item.id">{{ item.name }}</li></ul>
<h4>2. 数组 + 索引</h4>
<ul><li v-for="(item, i) in items" :key="item.id">{{ i + 1 }}. {{ item.name }}</li></ul>
<h4>3. 对象</h4>
<ul><li v-for="(value, key) in user" :key="key">{{ key }}: {{ value }}</li></ul>
<h4>4. 数字</h4>
<span v-for="n in 5" :key="n">[{{ n }}]</span>
<h4>5. 字符串</h4>
<span v-for="char in 'Hello'" :key="char">{{ char }} </span>
</div>
<script>
const { createApp, ref, reactive } = Vue
createApp({
setup() {
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
const user = reactive({ name: 'Alice', age: 25, email: 'alice@example.com' })
return { items, user }
}
}).mount('#app')
</script>
▶ 示例:5 种 :key 用法对比
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<h4>1. 唯一 ID(最佳)</h4>
<ul><li v-for="user in users" :key="user.id">{{ user.name }}</li></ul>
<h4>2. 唯一字段</h4>
<ul><li v-for="user in users" :key="user.email">{{ user.name }}</li></ul>
<h4>3. 复合 key</h4>
<ul><li v-for="post in posts" :key="`${post.userId}-${post.id}`">{{ post.title }}</li></ul>
<h4>4. 用 index(次选)</h4>
<ul><li v-for="(item, i) in items" :key="i">{{ item.name }}</li></ul>
<h4>5. ❌ random()(绝对不要)</h4>
<ul><li v-for="item in items" :key="Math.random()">{{ item.name }}</li></ul>
</div>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const users = ref([
{ id: 1, name: 'Alice', email: 'alice@x.com' },
{ id: 2, name: 'Bob', email: 'bob@x.com' }
])
const posts = ref([
{ userId: 1, id: 101, title: 'Post A' },
{ userId: 1, id: 102, title: 'Post B' }
])
const items = ref([{ name: 'X' }, { name: 'Y' }])
return { users, posts, items }
}
}).mount('#app')
</script>
▶ 示例:列表过滤 + 排序 + 分页完整示例
HTML
📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<style>
ul { list-style: none; padding: 0; }
li { padding: 6px 10px; border-bottom: 1px solid #eee; }
input, select { padding: 4px; margin: 4px; }
button { padding: 4px 12px; margin: 0 4px; }
button:disabled { opacity: 0.5; }
</style>
<div id="app">
<input v-model="searchQuery" placeholder="搜索...">
<select v-model="selectedCategory">
<option value="all">全部</option>
<option value="phone">手机</option>
<option value="laptop">笔记本</option>
</select>
<select v-model="sortBy">
<option value="price">价格</option>
<option value="name">名称</option>
</select>
<ul>
<li v-for="item in paginatedItems" :key="item.id">
{{ item.name }} - ${{ item.price }}
</li>
</ul>
<button :disabled="currentPage === 1" @click="currentPage--">上一页</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button :disabled="currentPage === totalPages" @click="currentPage++">下一页</button>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const items = ref([
{ id: 1, name: 'iPhone', price: 999, category: 'phone' },
{ id: 2, name: 'MacBook', price: 2499, category: 'laptop' },
{ id: 3, name: 'iPad', price: 599, category: 'tablet' }
])
const searchQuery = ref('')
const selectedCategory = ref('all')
const sortBy = ref('price')
const currentPage = ref(1)
const pageSize = 10
const filteredItems = computed(() => {
let result = items.value
if (searchQuery.value) {
result = result.filter(i => i.name.includes(searchQuery.value))
}
if (selectedCategory.value !== 'all') {
result = result.filter(i => i.category === selectedCategory.value)
}
return [...result].sort((a, b) => {
if (sortBy.value === 'price') return a.price - b.price
return a.name.localeCompare(b.name)
})
})
const paginatedItems = computed(() => {
const start = (currentPage.value - 1) * pageSize
return filteredItems.value.slice(start, start + pageSize)
})
const totalPages = computed(() =>
Math.ceil(filteredItems.value.length / pageSize)
)
return { searchQuery, selectedCategory, sortBy, currentPage,
paginatedItems, totalPages }
}
}).mount('#app')
</script>
▶ 示例:5 个常见错误
| 错误 | 现象 | 解决 |
|---|---|---|
| 没有 :key | 列表错位、性能差 | 加 :key="item.id" |
| v-for 和 v-if 一起用 | v-if 优先级混乱 | 用 template 包装或 computed |
| 用 index 当 key | 排序时错位 | 用唯一 ID |
v-show 不支持 <transition> |
不能动画 | 改用 v-if + <transition> |
| 列表太大(10万+) | 页面卡死 | 用 vue-virtual-scroller 虚拟滚动 |
▶ 示例:v-if 和 v-for 优先级
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<!-- ✅ 推荐:computed 过滤 -->
<ul>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</ul>
<button @click="toggleUser(1)">切换 Alice 状态</button>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const users = ref([
{ id: 1, name: 'Alice', isActive: true },
{ id: 2, name: 'Bob', isActive: false }
])
const activeUsers = computed(() =>
users.value.filter(u => u.isActive)
)
function toggleUser(id) {
const u = users.value.find(u => u.id === id)
if (u) u.isActive = !u.isActive
}
return { users, activeUsers, toggleUser }
}
}).mount('#app')
</script>
⚠️ Vue 3 中 v-if 优先级高于 v-for(Vue 2 相反)。官方推荐:不要一起用,用 template 分开或 computed 过滤。
❓ 常见问题
Q v-if 和 v-show 怎么选?
A 偶尔切换用 v-if(节省初始渲染),频繁切换用 v-show(避免反复销毁/重建 DOM)。登录/未登录用 v-if,tab 切换用 v-show。
Q v-for 必须用 :key 吗?
A 强烈推荐用 :key 避免性能问题和组件状态错位。用唯一 ID(item.id)最佳,index 次之,绝对不要用 random()。
Q v-if 和 v-for 一起用,优先级?
A Vue 3 中 v-if 优先级高于 v-for(Vue 2 相反)。但官方推荐不要一起用,用 template 分开或用 computed 过滤。
Q template 元素有什么用?
A template 是 Vue 逻辑包装元素,不渲染到 DOM。用于 v-if/v-for 包含多个元素时,避免多余的 div。
Q 列表性能差怎么办?
A 3 个优化:(1) 加 :key;(2) 用 computed 缓存过滤/排序结果;(3) 大列表(10万+)用 vue-virtual-scroller 虚拟滚动,只渲染可见区域。
Q v-for 遍历对象能拿到 key/value 吗?
A 能。
v-for="(value, key, index) in obj",参数顺序:值、键、索引。Q 怎么实现"无数据时显示提示"?
A 用 v-if/v-else 组合:
<div v-if="items.length">列表</div><div v-else>无数据</div>。Q 列表点击项高亮怎么实现?
A 给每个项加 activeIndex 比较:
@click="activeIndex = item.id" :class="{ active: activeIndex === item.id }"。📖 小节
- v-if 条件渲染(移除 DOM),v-show 切换 display(始终在 DOM)
- 偶尔切换用 v-if,频繁切换用 v-show
- v-for 遍历数组/对象/数字/字符串,用 :key 提升性能
- :key 用唯一 ID 最佳,index 次之,不用 random()
- template 元素包装多个元素,不渲染到 DOM
- 列表过滤/排序/分页用 computed 自动缓存
- Vue 3 中 v-if 优先级高于 v-for
- 大列表用虚拟滚动优化(10万+ 项)
📝 作业
-
基础题(难度⭐) 用 v-if/v-else 实现登录/未登录切换:
- 已登录:显示用户名和"登出"按钮
- 未登录:显示"请登录"和登录表单
- 用 template 包装多元素
-
进阶题(难度⭐⭐) 用 v-for 实现 Todo 列表:
- 3 个状态:全部 / 未完成 / 已完成(3 个按钮切换)
- 列表显示对应状态的任务
- 每项有"完成"和"删除"按钮
- 底部显示"未完成:N 项"
-
挑战题(难度⭐⭐⭐) 实现完整的商品筛选 + 排序 + 分页系统(5 个文件):
- 商品数据:30 个,含 name/price/category/createdAt
- 搜索:按 name 模糊搜索
- 分类筛选:5 个分类(all/phone/laptop/tablet/headphone)
- 排序:价格升序/降序/名称/最新
- 分页:10 项/页,显示页码导航
- 性能:computed 缓存过滤/排序结果
- 大列表优化:1000 项测试,确保不卡