跳到主要内容

Vue 条件渲染

条件渲染是前端开发中非常常见的需求。Vue 提供了 v-ifv-elsev-else-ifv-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>

awesometrue 时,<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-elsev-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-ifv-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-elsev-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-ifv-show
DOM 渲染条件为假时不渲染始终渲染
切换方式销毁和重建组件CSS display 切换
初始渲染开销低(惰性渲染)高(总是渲染)
切换开销高(销毁/重建)低(CSS 切换)
支持 template
支持 v-else

选择指南

使用 v-if 的场景

  1. 条件很少改变:避免不必要的初始渲染开销
  2. 条件在运行时很少改变:如权限判断、用户角色判断
  3. 需要配合 v-else:有 else 分支的场景
  4. 初始条件为假:惰性渲染节省资源
<script setup>
import { ref } from 'vue'

const isLoggedIn = ref(false)
</script>

<template>
<!-- 使用 v-if:登录状态很少改变 -->
<UserProfile v-if="isLoggedIn" />
<LoginForm v-else />
</template>

使用 v-show 的场景

  1. 需要频繁切换:如标签页、手风琴、下拉菜单
  2. 条件可能在初始渲染时为真:避免重复渲染开销
<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-ifv-for,因为二者的优先级不明显。

v-ifv-for 一起使用时,v-if 的优先级比 v-for 更高。这意味着:

<!-- 无效:v-if 优先级更高,无法访问 todo 变量 -->
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo.name }}
</li>

上面的代码会报错,因为 v-ifv-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 条件渲染的完整内容:

  1. v-if:真正的条件渲染,惰性执行,条件为假时不渲染
  2. v-else:配合 v-if 使用,表示 else 分支
  3. v-else-if:多条件判断链
  4. v-show:基于 CSS 的显示切换,始终渲染到 DOM
  5. template 支持:v-if 系列支持 template,v-show 不支持
  6. key 属性:控制元素是否复用,避免状态残留
  7. v-if 与 v-for:避免同时使用,推荐使用计算属性过滤

选择建议

  • 需要频繁切换,使用 v-show
  • 条件很少改变,使用 v-if
  • 需要多分支判断,使用 v-if + v-else-if + v-else
  • 初始条件为假且组件昂贵,使用 v-if

练习

  1. 创建一个用户登录状态切换的示例,显示不同的欢迎信息
  2. 实现一个带有动画效果的标签页组件
  3. 创建一个加载状态管理组件,包含加载、成功、错误三种状态

准备好进入下一章,学习列表渲染的内容了吗?