Vue 条件渲染
条件渲染是前端开发中非常常见的需求。Vue 提供了 v-if、v-else、v-else-if 和 v-show 指令来实现条件渲染。理解它们之间的区别和适用场景,对于编写高效、可维护的 Vue 应用至关重要。
v-if 指令
v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时被渲染。
基本用法
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
</script>
<template>
<h1 v-if="awesome">Vue is awesome!</h1>
</template>
当 awesome 为 true 时,<h1> 元素会被渲染;为 false 时,该元素不会被渲染到 DOM 中。
v-if 的特点
v-if 是"真正"的条件渲染,因为它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
<script setup>
import { ref } from 'vue'
// 初始为 false,组件不会被创建
const showComponent = ref(false)
// 2秒后显示组件
setTimeout(() => {
showComponent.value = true
}, 2000)
</script>
<template>
<!-- 组件在条件变为 true 之前不会被创建 -->
<ExpensiveComponent v-if="showComponent" />
</template>
v-else 指令
你可以使用 v-else 为 v-if 添加一个"else 块":
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
</script>
<template>
<button @click="awesome = !awesome">切换</button>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no</h1>
</template>
v-else 元素必须紧跟在带 v-if 或者 v-else-if 的元素的后面,否则它将不会被识别。
v-else-if 指令
v-else-if 顾名思义,充当 v-if 的"else if 块",可以连续使用:
<script setup>
import { ref } from 'vue'
const type = ref('B')
</script>
<template>
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else-if="type === 'C'">C</div>
<div v-else>Not A/B/C</div>
</template>
与 v-else 类似,v-else-if 也必须紧跟在带 v-if 或 v-else-if 的元素之后。
在 template 上使用 v-if
因为 v-if 是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template> 元素当做不可见的包裹元素,并在上面使用 v-if。最终的渲染结果将不包含 <template> 元素。
<script setup>
import { ref } from 'vue'
const ok = ref(true)
</script>
<template>
<template v-if="ok">
<h1>标题</h1>
<p>段落 1</p>
<p>段落 2</p>
</template>
</template>
v-else 和 v-else-if 也可以在 <template> 上使用:
<template>
<template v-if="type === 'A'">
<h1>类型 A</h1>
<p>这是类型 A 的内容</p>
</template>
<template v-else-if="type === 'B'">
<h1>类型 B</h1>
<p>这是类型 B 的内容</p>
</template>
<template v-else>
<h1>其他类型</h1>
<p>这是其他类型的内容</p>
</template>
</template>
v-show 指令
另一个用于根据条件展示元素的选项是 v-show 指令。用法大致一样:
<script setup>
import { ref } from 'vue'
const ok = ref(true)
</script>
<template>
<h1 v-show="ok">Hello!</h1>
</template>
v-show 的特点
不同的是带有 v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS 属性 display。
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
</script>
<template>
<!-- v-show 只是切换 display 属性 -->
<div v-show="isVisible">这个元素始终存在于 DOM 中</div>
<!-- 等价于 -->
<div :style="{ display: isVisible ? 'block' : 'none' }">
这个元素也始终存在于 DOM 中
</div>
</template>
v-show 的限制
v-show 不支持 <template> 元素,也不支持 v-else。
<!-- 无效:v-show 不能用在 template 上 -->
<template v-show="ok">
<p>内容</p>
</template>
v-if vs v-show
核心区别
| 特性 | v-if | v-show |
|---|---|---|
| DOM 渲染 | 条件为假时不渲染 | 始终渲染 |
| 切换方式 | 销毁和重建组件 | CSS display 切换 |
| 初始渲染开销 | 低(惰性渲染) | 高(总是渲染) |
| 切换开销 | 高(销毁/重建) | 低(CSS 切换) |
| 支持 template | 是 | 否 |
| 支持 v-else | 是 | 否 |
选择指南
使用 v-if 的场景:
- 条件很少改变:避免不必要的初始渲染开销
- 条件在运行时很少改变:如权限判断、用户角色判断
- 需要配合 v-else:有 else 分支的场景
- 初始条件为假:惰性渲染节省资源
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
</script>
<template>
<!-- 使用 v-if:登录状态很少改变 -->
<UserProfile v-if="isLoggedIn" />
<LoginForm v-else />
</template>
使用 v-show 的场景:
- 需要频繁切换:如标签页、手风琴、下拉菜单
- 条件可能在初始渲染时为真:避免重复渲染开销
<script setup>
import { ref } from 'vue'
const activeTab = ref('home')
</script>
<template>
<!-- 使用 v-show:标签页频繁切换 -->
<div class="tabs">
<div v-show="activeTab === 'home'" class="tab">首页内容</div>
<div v-show="activeTab === 'profile'" class="tab">个人资料</div>
<div v-show="activeTab === 'settings'" class="tab">设置</div>
</div>
</template>
性能对比
<script setup>
import { ref } from 'vue'
// 场景1:频繁切换(使用 v-show 更好)
const showTooltip = ref(false)
// 场景2:条件很少改变(使用 v-if 更好)
const hasPermission = ref(false)
</script>
<template>
<div>
<!-- 频繁切换的场景:使用 v-show -->
<button @mouseenter="showTooltip = true" @mouseleave="showTooltip = false">
悬停查看提示
</button>
<div v-show="showTooltip" class="tooltip">
这是一个提示信息
</div>
<!-- 条件很少改变的场景:使用 v-if -->
<div v-if="hasPermission">
<button @click="deleteAll">删除所有数据</button>
</div>
</div>
</template>
条件渲染与 key
Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。这带来性能优势,但有时需要特别注意。
元素复用的问题
<script setup>
import { ref } from 'vue'
const loginType = ref('username')
</script>
<template>
<template v-if="loginType === 'username'">
<label>用户名</label>
<input placeholder="请输入用户名" />
</template>
<template v-else>
<label>邮箱</label>
<input placeholder="请输入邮箱" />
</template>
<button @click="loginType = loginType === 'username' ? 'email' : 'username'">
切换登录方式
</button>
</template>
在上面的代码中切换 loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input> 不会被替换掉——仅仅是替换了它的 placeholder。
使用 key 管理可复用元素
如果要求每次切换时输入框都要被清空,可以给每个输入框添加唯一的 key 属性:
<script setup>
import { ref } from 'vue'
const loginType = ref('username')
</script>
<template>
<template v-if="loginType === 'username'">
<label>用户名</label>
<input key="username-input" placeholder="请输入用户名" />
</template>
<template v-else>
<label>邮箱</label>
<input key="email-input" placeholder="请输入邮箱" />
</template>
<button @click="loginType = loginType === 'username' ? 'email' : 'username'">
切换登录方式
</button>
</template>
添加 key 后,每次切换都会创建新的输入框,而不是复用。注意,<label> 元素仍然会被复用,因为它们没有添加 key 属性。
v-if 与 v-for 一起使用
不推荐在同一元素上使用 v-if 和 v-for,因为二者的优先级不明显。
当 v-if 与 v-for 一起使用时,v-if 的优先级比 v-for 更高。这意味着:
<!-- 无效:v-if 优先级更高,无法访问 todo 变量 -->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>
上面的代码会报错,因为 v-if 在 v-for 之前评估,此时 todo 变量还不存在。
正确做法
方式一:使用 <template> 将 v-for 移到外层
<script setup>
import { ref } from 'vue'
const todos = ref([
{ name: '学习 Vue', isComplete: false },
{ name: '做作业', isComplete: true },
{ name: '锻炼身体', isComplete: false }
])
</script>
<template>
<template v-for="todo in todos" :key="todo.name">
<li v-if="!todo.isComplete">
{{ todo.name }}
</li>
</template>
</template>
方式二:使用计算属性过滤(推荐)
<script setup>
import { ref, computed } from 'vue'
const todos = ref([
{ name: '学习 Vue', isComplete: false },
{ name: '做作业', isComplete: true },
{ name: '锻炼身体', isComplete: false }
])
// 使用计算属性预先过滤
const incompleteTodos = computed(() => {
return todos.value.filter(todo => !todo.isComplete)
})
</script>
<template>
<li v-for="todo in incompleteTodos" :key="todo.name">
{{ todo.name }}
</li>
</template>
方式三:将 v-if 移到容器元素
如果目的是根据条件隐藏整个列表:
<script setup>
import { ref } from 'vue'
const todos = ref([...])
const shouldShowTodos = ref(true)
</script>
<template>
<!-- 将 v-if 移到容器 -->
<ul v-if="shouldShowTodos">
<li v-for="todo in todos" :key="todo.name">
{{ todo.name }}
</li>
</ul>
</template>
实际应用示例
示例:显示/隐藏密码
<script setup>
import { ref } from 'vue'
const password = ref('')
const showPassword = ref(false)
</script>
<template>
<div class="password-input">
<input
:type="showPassword ? 'text' : 'password'"
v-model="password"
placeholder="请输入密码"
/>
<button @click="showPassword = !showPassword">
{{ showPassword ? '隐藏' : '显示' }}
</button>
</div>
</template>
<style scoped>
.password-input {
display: flex;
gap: 8px;
}
input {
padding: 8px;
flex: 1;
}
button {
padding: 8px 16px;
}
</style>
示例:多条件评分系统
<script setup>
import { ref, computed } from 'vue'
const score = ref(85)
const grade = computed(() => {
if (score.value >= 90) return { level: 'A', text: '优秀', color: '#52c41a' }
if (score.value >= 80) return { level: 'B', text: '良好', color: '#1890ff' }
if (score.value >= 70) return { level: 'C', text: '中等', color: '#faad14' }
if (score.value >= 60) return { level: 'D', text: '及格', color: '#fa8c16' }
return { level: 'F', text: '不及格', color: '#f5222d' }
})
</script>
<template>
<div class="grade-system">
<h2>成绩评定系统</h2>
<div class="score-input">
<label>分数:</label>
<input type="number" v-model.number="score" min="0" max="100" />
</div>
<div class="result">
<p>分数:{{ score }}</p>
<p>等级:<span :style="{ color: grade.color }">{{ grade.level }}</span></p>
<p>评价:{{ grade.text }}</p>
</div>
<div class="grade-bar">
<div v-if="score >= 90" class="bar excellent">优秀 (90-100)</div>
<div v-else-if="score >= 80" class="bar good">良好 (80-89)</div>
<div v-else-if="score >= 70" class="bar medium">中等 (70-79)</div>
<div v-else-if="score >= 60" class="bar pass">及格 (60-69)</div>
<div v-else class="bar fail">不及格 (0-59)</div>
</div>
</div>
</template>
<style scoped>
.grade-system {
max-width: 400px;
margin: 0 auto;
padding: 20px;
}
.score-input {
margin: 20px 0;
}
input {
padding: 8px;
width: 100px;
}
.result {
margin: 20px 0;
padding: 15px;
background: #f5f5f5;
border-radius: 4px;
}
.grade-bar .bar {
padding: 10px;
text-align: center;
color: white;
border-radius: 4px;
}
.excellent { background: #52c41a; }
.good { background: #1890ff; }
.medium { background: #faad14; }
.pass { background: #fa8c16; }
.fail { background: #f5222d; }
</style>
示例:标签页切换
<script setup>
import { ref } from 'vue'
const activeTab = ref('home')
const tabs = [
{ id: 'home', label: '首页', content: '这是首页内容,欢迎来到我们的网站!' },
{ id: 'profile', label: '个人资料', content: '这是个人资料页面。' },
{ id: 'settings', label: '设置', content: '这是设置页面,您可以在这里配置各种选项。' }
]
</script>
<template>
<div class="tabs-container">
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.id"
:class="['tab-btn', { active: activeTab === tab.id }]"
@click="activeTab = tab.id"
>
{{ tab.label }}
</button>
</div>
<div class="tab-content">
<div v-if="activeTab === 'home'" class="tab-panel">
<h3>首页</h3>
<p>{{ tabs[0].content }}</p>
</div>
<div v-else-if="activeTab === 'profile'" class="tab-panel">
<h3>个人资料</h3>
<p>{{ tabs[1].content }}</p>
</div>
<div v-else-if="activeTab === 'settings'" class="tab-panel">
<h3>设置</h3>
<p>{{ tabs[2].content }}</p>
</div>
</div>
</div>
</template>
<style scoped>
.tabs-container {
max-width: 500px;
margin: 0 auto;
}
.tab-buttons {
display: flex;
border-bottom: 1px solid #ddd;
}
.tab-btn {
padding: 10px 20px;
background: none;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.3s;
}
.tab-btn:hover {
background: #f5f5f5;
}
.tab-btn.active {
color: #42b883;
border-bottom-color: #42b883;
}
.tab-content {
padding: 20px;
min-height: 100px;
}
</style>
示例:加载状态管理
<script setup>
import { ref } from 'vue'
const loading = ref(false)
const error = ref(null)
const data = ref(null)
async function fetchData() {
loading.value = true
error.value = null
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 1500))
data.value = { name: '张三', email: '[email protected]' }
} catch (e) {
error.value = '加载失败,请重试'
} finally {
loading.value = false
}
}
</script>
<template>
<div class="data-loader">
<button @click="fetchData" :disabled="loading">
{{ loading ? '加载中...' : '获取数据' }}
</button>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<span class="spinner"></span>
<p>正在加载数据...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-state">
<p>{{ error }}</p>
<button @click="fetchData">重试</button>
</div>
<!-- 成功状态 -->
<div v-else-if="data" class="success">
<h3>用户信息</h3>
<p>姓名:{{ data.name }}</p>
<p>邮箱:{{ data.email }}</p>
</div>
<!-- 初始状态 -->
<div v-else class="initial">
<p>点击按钮获取数据</p>
</div>
</div>
</template>
<style scoped>
.data-loader {
max-width: 400px;
margin: 0 auto;
text-align: center;
}
.loading {
padding: 20px;
}
.spinner {
display: inline-block;
width: 30px;
height: 30px;
border: 3px solid #f3f3f3;
border-top: 3px solid #42b883;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-state {
padding: 20px;
color: #f5222d;
}
.success {
padding: 20px;
background: #f6ffed;
border: 1px solid #b7eb8f;
border-radius: 4px;
}
.initial {
padding: 20px;
color: #999;
}
button {
padding: 10px 20px;
background: #42b883;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
</style>
小结
本章我们详细学习了 Vue 条件渲染的完整内容:
- v-if:真正的条件渲染,惰性执行,条件为假时不渲染
- v-else:配合 v-if 使用,表示 else 分支
- v-else-if:多条件判断链
- v-show:基于 CSS 的显示切换,始终渲染到 DOM
- template 支持:v-if 系列支持 template,v-show 不支持
- key 属性:控制元素是否复用,避免状态残留
- v-if 与 v-for:避免同时使用,推荐使用计算属性过滤
选择建议:
- 需要频繁切换,使用
v-show - 条件很少改变,使用
v-if - 需要多分支判断,使用
v-if+v-else-if+v-else - 初始条件为假且组件昂贵,使用
v-if
练习
- 创建一个用户登录状态切换的示例,显示不同的欢迎信息
- 实现一个带有动画效果的标签页组件
- 创建一个加载状态管理组件,包含加载、成功、错误三种状态
准备好进入下一章,学习列表渲染的内容了吗?