条件渲染与列表渲染
页面不是静态的:数据决定内容是否出现,列表决定内容如何重复。掌握条件渲染与列表渲染,就掌握了动态页面的基本骨架。
在上一篇文章中,我们学习了计算属性与侦听器。本文将进一步解决两个高频问题:如何根据状态切换页面内容,以及如何正确、高效地渲染一组数据。
写作约定:本系列以 Vue 3
<script setup>为主要写法。示例默认使用 Vue 3.2+,重点关注实际开发中的选择依据与常见陷阱。
一、条件渲染:让数据决定页面结构
条件渲染并不只是“显示或隐藏一个元素”。它还会影响到 DOM 是否创建、组件是否初始化,以及切换时需要付出多少性能成本。
1.1 v-if、v-else-if 与 v-else
v-if 会根据表达式的真假决定是否渲染元素。多个分支可以组合成完整的条件链:
<template>
<p v-if="status === 'loading'">正在加载...</p>
<p v-else-if="status === 'error'">加载失败,请稍后重试</p>
<p v-else-if="status === 'empty'">暂无数据</p>
<p v-else>数据加载完成</p>
</template>
<script setup>
import { ref } from 'vue'
const status = ref('loading')
</script>这些分支必须紧跟在一起,中间不能插入其他元素,否则 v-else-if 或 v-else 将无法识别前面的 v-if。
当一组元素需要整体控制时,可以把指令放在 <template> 上:
<template>
<button v-if="!isLoggedIn" @click="isLoggedIn = true">登录</button>
<template v-else>
<h2>欢迎回来</h2>
<p>你可以继续管理个人资料。</p>
<button @click="isLoggedIn = false">退出登录</button>
</template>
</template>
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>这里的 <template> 只是分组容器,不会作为额外节点出现在最终 DOM 中。
1.2 v-show:保留 DOM,只切换显示状态
v-show 不论条件真假都会渲染元素,条件为假时通过 CSS 的 display: none 隐藏:
<template>
<button @click="visible = !visible">切换帮助信息</button>
<div v-show="visible" class="help-panel">
这里是经常展开和收起的帮助内容。
</div>
</template>
<script setup>
import { ref } from 'vue'
const visible = ref(true)
</script>v-show 不支持在 <template> 上使用,也不能与 v-else 组成条件链。
1.3 v-if 与 v-show 如何选择
可以记住一个简单原则:很少切换用 v-if,频繁切换用 v-show,初始开销和组件生命周期也要一起考虑。
1.4 使用 key 区分不同分支
Vue 会尽量复用更新前后的同类型元素。如果两个分支结构相似,但业务含义不同,应该用 key 明确区分:
<template>
<input
v-if="loginType === 'username'"
key="username-input"
placeholder="输入用户名"
>
<input
v-else
key="email-input"
placeholder="输入邮箱"
>
<button @click="toggleLoginType">切换登录方式</button>
</template>
<script setup>
import { ref } from 'vue'
const loginType = ref('username')
const toggleLoginType = () => {
loginType.value = loginType.value === 'username' ? 'email' : 'username'
}
</script>加上不同的 key 后,Vue 会替换输入框,而不是继续复用旧节点。这能避免旧输入内容、焦点或组件内部状态被意外保留。
二、列表渲染:用 v-for 处理一组数据
v-for 基于源数据重复渲染元素。基本语法是 item in items,也可以使用 of 代替 in,使其更接近 JavaScript 迭代语法。
2.1 遍历数组
<template>
<ul>
<li v-for="(book, index) in books" :key="book.id">
{{ index + 1 }}.《{{ book.title }}》
</li>
</ul>
</template>
<script setup>
import { ref } from 'vue'
const books = ref([
{ id: 1, title: 'Vue.js设计与实现' },
{ id: 2, title: 'JavaScript高级程序设计' },
{ id: 3, title: '深入浅出Vue.js' }
])
</script>book 是当前项,index 是从 0 开始的索引。业务渲染应优先使用当前项中的稳定 ID 作为 key。
2.2 遍历对象与整数
遍历对象时,可以依次取得值、键和索引:
<template>
<ul>
<li v-for="(value, key) in user" :key="key">
{{ key }}:{{ value }}
</li>
</ul>
<div>
<span v-for="page in 5" :key="page" class="page">{{ page }}</span>
</div>
</template>
<script setup>
const user = {
name: '张三',
role: '前端工程师',
city: '济南'
}
</script>v-for="page in 5" 会从 1 开始,依次得到 1 到 5,而不是 0 到 4。
2.3 在 template 上渲染多个元素
当每个数据项对应多个兄弟元素时,可以使用 <template v-for>,并把 key 放在 <template> 上:
<template>
<dl>
<template v-for="item in descriptions" :key="item.id">
<dt>{{ item.term }}</dt>
<dd>{{ item.description }}</dd>
</template>
</dl>
</template>
<script setup>
const descriptions = [
{ id: 1, term: 'v-if', description: '按条件创建或销毁内容' },
{ id: 2, term: 'v-for', description: '根据数据重复渲染内容' }
]
</script>这样既不需要多余的包裹节点,也能让每一组 dt、dd 拥有明确的身份。
三、key 的真正作用:帮助 Vue 识别节点
key 不是普通的 HTML 属性,而是 Vue 虚拟 DOM Diff 算法识别节点的依据。它回答的是:更新前后,哪些节点仍然代表同一个数据项?
3.1 为什么推荐稳定且唯一的 ID
假设原列表是 [A, B, C],现在在开头插入 D。如果使用索引作为 key,各项的 key 与数据对应关系就会变化:
更新前:A(key=0) B(key=1) C(key=2)
更新后:D(key=0) A(key=1) B(key=2) C(key=3)Vue 可能把原来的 A 节点更新成 D,把 B 更新成 A,而不是简单复用 A、B、C。普通文本通常仍能正确更新,但输入框、子组件内部状态或焦点可能出现错位。
<template>
<!-- 推荐:ID 在数据生命周期内保持稳定 -->
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</template>索引并非绝对不能用。对于不会排序、过滤、插入或删除,且节点没有独立状态的静态列表,索引通常可以接受。一旦列表会动态变化,就应使用业务 ID。
3.2 key 应该放在哪里
普通元素直接把 key 放在元素上;组件列表把 key 放在组件上;<template v-for> 则把 key 放在 <template> 上:
<template>
<TodoItem v-for="todo in todos" :key="todo.id" :todo="todo" />
</template>不要把 key 当成列表样式或业务标识传给子组件,它的主要职责是辅助 Vue 正确复用节点。
四、条件渲染与列表渲染的组合
4.1 不建议在同一元素上混用 v-if 与 v-for
在 Vue 3 中,同一元素上的 v-if 优先级高于 v-for,因此 v-if 往往无法取得当前循环项。即使改成依赖外部条件,混合写法也会让意图变得不清晰。
不推荐:
<template>
<li v-for="user in users" v-if="user.active" :key="user.id">
{{ user.name }}
</li>
</template>更推荐先通过计算属性得到需要展示的数据:
<template>
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
</template>
<script setup>
import { computed, ref } from 'vue'
const users = ref([
{ id: 1, name: '张三', active: true },
{ id: 2, name: '李四', active: false },
{ id: 3, name: '王五', active: true }
])
const activeUsers = computed(() => users.value.filter(user => user.active))
</script>这样条件逻辑与循环结构各自独立,也更容易测试和维护。
4.2 处理加载、空列表与正常数据
实际页面通常不只有“显示”和“隐藏”两种状态,还应考虑加载、失败和空结果:
<template>
<p v-if="loading">正在加载用户...</p>
<p v-else-if="errorMessage">{{ errorMessage }}</p>
<ul v-else-if="users.length">
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
<p v-else>暂无用户</p>
</template>
<script setup>
import { ref } from 'vue'
const loading = ref(false)
const errorMessage = ref('')
const users = ref([])
</script>把状态分支写完整,可以避免页面在请求过程中或没有数据时出现空白。
五、列表更新与性能注意事项
Vue 3 能侦测 push、pop、splice、sort、reverse 等数组变更,也能侦测通过索引赋值和修改 length。需要关注的是更新方式是否符合业务意图,而不是沿用 Vue 2 的旧限制。
5.1 修改原数组与替换数组
两种方式都可以触发更新:
<script setup>
import { ref } from 'vue'
const todos = ref([{ id: 1, title: '学习v-for' }])
// 修改原数组,适合局部增删
const addTodo = () => {
todos.value.push({ id: 2, title: '学习key' })
}
// 替换数组,适合过滤、排序和不可变更新
const removeTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id)
}
</script>排序时要特别注意:sort() 会修改原数组。如果只想改变展示顺序,不想改变源数据,应先复制再排序:
<script setup>
import { computed, ref } from 'vue'
const products = ref([
{ id: 1, name: '键盘', price: 199 },
{ id: 2, name: '鼠标', price: 99 }
])
const productsByPrice = computed(() => {
return [...products.value].sort((a, b) => a.price - b.price)
})
</script>5.2 不要只追求“复用越多越好”
正确的 key 能减少不必要的 DOM 操作,但 key 的首要价值是保证渲染结果和组件状态正确。如果业务上需要强制重建一个组件,也可以主动改变它的 key;不要为了追求复用而让不同数据项共享错误状态。
六、综合实战:商品搜索、筛选与排序
下面把条件渲染、列表渲染和计算属性组合起来,实现一个完整的商品列表示例:
<template>
<section class="product-panel">
<h2>商品列表</h2>
<div class="toolbar">
<input v-model.trim="keyword" placeholder="搜索商品名称">
<label>
<input v-model="stockOnly" type="checkbox">
仅看有货
</label>
<select v-model="sortType">
<option value="default">默认排序</option>
<option value="priceAsc">价格从低到高</option>
<option value="priceDesc">价格从高到低</option>
</select>
</div>
<p v-if="displayProducts.length === 0" class="empty">
没有找到符合条件的商品
</p>
<ul v-else class="product-list">
<li v-for="product in displayProducts" :key="product.id">
<div>
<strong>{{ product.name }}</strong>
<span>¥{{ product.price }}</span>
</div>
<span :class="product.stock > 0 ? 'in-stock' : 'out-of-stock'">
{{ product.stock > 0 ? `库存 ${product.stock}` : '暂时缺货' }}
</span>
</li>
</ul>
</section>
</template>
<script setup>
import { computed, ref } from 'vue'
const products = ref([
{ id: 1, name: '机械键盘', price: 299, stock: 12 },
{ id: 2, name: '无线鼠标', price: 129, stock: 0 },
{ id: 3, name: '显示器', price: 1099, stock: 5 },
{ id: 4, name: '扩展坞', price: 399, stock: 8 }
])
const keyword = ref('')
const stockOnly = ref(false)
const sortType = ref('default')
const filteredProducts = computed(() => {
const searchText = keyword.value.toLowerCase()
return products.value.filter(product => {
const matchesKeyword = product.name.toLowerCase().includes(searchText)
const matchesStock = !stockOnly.value || product.stock > 0
return matchesKeyword && matchesStock
})
})
const displayProducts = computed(() => {
const result = [...filteredProducts.value]
if (sortType.value === 'priceAsc') {
return result.sort((a, b) => a.price - b.price)
}
if (sortType.value === 'priceDesc') {
return result.sort((a, b) => b.price - a.price)
}
return result
})
</script>
<style scoped>
.product-panel {
max-width: 680px;
margin: 24px auto;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
margin-bottom: 16px;
}
.product-list {
padding: 0;
list-style: none;
}
.product-list li {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
border-bottom: 1px solid #e5e7eb;
}
.product-list li div {
display: flex;
gap: 16px;
}
.empty {
padding: 32px;
color: #6b7280;
text-align: center;
}
.in-stock {
color: #059669;
}
.out-of-stock {
color: #dc2626;
}
</style>这个例子中有三个值得保留的思路:
过滤与排序分开:
filteredProducts负责筛选,displayProducts负责展示顺序。不修改源数组:排序前先复制,避免展示逻辑意外改变
products。空状态明确:没有符合条件的数据时给出提示,而不是只渲染一个空列表。
七、常见问题自查
八、总结
条件渲染和列表渲染看似是基础语法,却直接影响页面的正确性、组件状态和渲染性能。本文最重要的几点是:
v-if控制是否创建内容,v-show控制是否显示内容。key的核心作用是帮助 Vue 识别节点,动态列表优先使用稳定 ID。不在同一元素上混用
v-if和v-for,优先用计算属性表达过滤逻辑。排序、过滤时明确是否修改源数据,避免隐式副作用。
完整页面应同时考虑加载、失败、空结果和正常数据状态。
下一站预告
下一篇《事件处理与表单输入绑定》将继续完善交互能力,深入讲解事件修饰符、按键处理、v-model 以及常见表单场景。
作者:洋洋技术笔记 发布日期:2026-11-01 系列:Vue.js从入门到精通 - 第5篇
评论区