Vue3 v-if、v-show、v-for 详解及示例
参考
示例
- 登录后标签页
- 成绩评级及角色权限
- 遍历:数组、对象、数字
- 水果购物车
核心概念理解
这三个指令都是 Vue 的条件渲染和列表渲染指令,但它们的工作原理和使用场景完全不同。
v-if vs v-show
基本区别
vue
<template>
<div>
<h2>v-if vs v-show 对比</h2>
<!-- v-if:条件为 false 时,元素完全不存在于 DOM 中 -->
<div v-if="isVisible" class="box v-if-box">
我是 v-if 控制的元素
</div>
<!-- v-show:条件为 false 时,元素仍在 DOM 中,只是 display: none -->
<div v-show="isVisible" class="box v-show-box">
我是 v-show 控制的元素
</div>
<button @click="toggleVisibility">
切换显示状态
</button>
<div class="debug-info">
<p>v-if 元素在 DOM 中: {{ isVifInDOM }}</p>
<p>v-show 元素在 DOM 中: {{ isVshowInDOM }}</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
const isVisible = ref(true)
const toggleVisibility = () => {
isVisible.value = !isVisible.value
}
// 检查元素是否在 DOM 中(用于演示)
const isVifInDOM = ref(false)
const isVshowInDOM = ref(false)
onMounted(() => {
updateDOMStatus()
})
const updateDOMStatus = async () => {
await nextTick()
isVifInDOM.value = !!document.querySelector('.v-if-box')
isVshowInDOM.value = !!document.querySelector('.v-show-box')
}
// 监听 isVisible 变化
import { watch } from 'vue'
watch(isVisible, () => {
setTimeout(updateDOMStatus, 0)
})
</script>
<style>
.box {
padding: 20px;
margin: 10px 0;
border-radius: 8px;
text-align: center;
font-weight: bold;
}
.v-if-box {
background-color: #d1ecf1;
border: 2px solid #bee5eb;
color: #0c5460;
}
.v-show-box {
background-color: #d4edda;
border: 2px solid #c3e6cb;
color: #155724;
}
.debug-info {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
</style>
详细对比表格
特性 | v-if | v-show |
---|---|---|
渲染方式 | 条件为 false 时不渲染 | 始终渲染,通过 CSS 控制显示 |
DOM 操作 | 频繁切换时开销大 | 切换开销小 |
初始渲染 | 条件为 false 时无开销 | 始终有开销 |
适用场景 | 很少切换的条件 | 频繁切换的条件 |
实际应用场景
vue
<template>
<div class="condition-demo">
<h2>条件渲染实际应用</h2>
<!-- 用户登录状态显示 -->
<div class="user-section">
<div v-if="isLoggedIn">
<h3>欢迎回来,{{ userInfo.name }}!</h3>
<button @click="logout">退出登录</button>
</div>
<div v-else>
<h3>请登录</h3>
<button @click="login">登录</button>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-section">
<!-- 加载中 - 使用 v-if,因为不经常切换 -->
<div v-if="isLoading" class="loading">
<div class="spinner"></div>
<p>加载中...</p>
</div>
<!-- 内容显示 - 使用 v-show,可能频繁切换 -->
<div v-show="!isLoading && !isError" class="content">
<h3>内容区域</h3>
<p>这里是主要内容...</p>
</div>
<!-- 错误提示 - 使用 v-if,因为不经常出现 -->
<div v-if="isError" class="error">
<h3>❌ 加载失败</h3>
<p>{{ errorMessage }}</p>
<button @click="retry">重试</button>
</div>
</div>
<!-- Tab 切换 - 使用 v-show,因为频繁切换 -->
<div class="tab-demo">
<div class="tab-buttons">
<button
v-for="tab in tabs"
:key="tab.id"
:class="{ active: activeTab === tab.id }"
@click="activeTab = tab.id"
>
{{ tab.name }}
</button>
</div>
<div class="tab-content">
<div v-show="activeTab === 'home'" class="tab-pane">
<h3>首页内容</h3>
<p>欢迎来到首页</p>
</div>
<div v-show="activeTab === 'profile'" class="tab-pane">
<h3>个人资料</h3>
<p>这里是个人资料页面</p>
</div>
<div v-show="activeTab === 'settings'" class="tab-pane">
<h3>设置</h3>
<p>系统设置页面</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isLoggedIn = ref(false)
const isLoading = ref(false)
const isError = ref(false)
const errorMessage = ref('')
const userInfo = ref({
name: 'Alice',
email: 'alice@example.com'
})
const activeTab = ref('home')
const tabs = ref([
{ id: 'home', name: '首页' },
{ id: 'profile', name: '个人资料' },
{ id: 'settings', name: '设置' }
])
const login = () => {
isLoading.value = true
// 模拟异步登录
setTimeout(() => {
isLoggedIn.value = true
isLoading.value = false
}, 1000)
}
const logout = () => {
isLoggedIn.value = false
}
const retry = () => {
isLoading.value = true
isError.value = false
// 模拟重试
setTimeout(() => {
// 随机模拟成功或失败
if (Math.random() > 0.5) {
isLoading.value = false
} else {
isLoading.value = false
isError.value = true
errorMessage.value = '网络连接失败,请稍后重试'
}
}, 1000)
}
</script>
<style>
.condition-demo {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.user-section {
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
margin-bottom: 20px;
text-align: center;
}
.loading-section {
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
margin-bottom: 20px;
min-height: 150px;
}
.loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100px;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #007bff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.content {
padding: 20px;
background-color: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 8px;
}
.error {
padding: 20px;
background-color: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 8px;
text-align: center;
}
.tab-demo {
margin-top: 30px;
}
.tab-buttons {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.tab-buttons button {
padding: 10px 20px;
border: none;
border-radius: 4px;
background-color: #e9ecef;
cursor: pointer;
transition: all 0.2s ease;
}
.tab-buttons button:hover {
background-color: #dee2e6;
}
.tab-buttons button.active {
background-color: #007bff;
color: white;
}
.tab-pane {
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
</style>
v-if 的配套指令
v-else 和 v-else-if
vue
<template>
<div class="if-else-demo">
<h2>v-if 配套指令</h2>
<div class="score-input">
<label>输入分数 (0-100):</label>
<input
type="number"
v-model.number="score"
min="0"
max="100"
>
</div>
<div class="grade-display">
<!-- 多重条件判断 -->
<div v-if="score >= 90" class="grade excellent">
<h3>🏆 优秀 (A)</h3>
<p>成绩优异,继续保持!</p>
</div>
<div v-else-if="score >= 80" class="grade good">
<h3>👍 良好 (B)</h3>
<p>不错的表现,还有提升空间</p>
</div>
<div v-else-if="score >= 70" class="grade average">
<h3>👌 一般 (C)</h3>
<p>需要更加努力哦</p>
</div>
<div v-else-if="score >= 60" class="grade poor">
<h3>⚠️ 及格 (D)</h3>
<p>刚好及格,要加油了</p>
</div>
<div v-else-if="score >= 0" class="grade fail">
<h3>❌ 不及格 (F)</h3>
<p>需要认真复习</p>
</div>
<div v-else class="grade empty">
<h3>📝 请输入分数</h3>
<p>请输入有效的分数</p>
</div>
</div>
<!-- 权限控制示例 -->
<div class="permission-demo">
<h3>用户权限演示</h3>
<select v-model="userRole">
<option value="guest">访客</option>
<option value="user">普通用户</option>
<option value="admin">管理员</option>
<option value="super">超级管理员</option>
</select>
<div class="permission-content">
<div v-if="userRole === 'super'" class="admin-content">
<h4>👑 超级管理员权限</h4>
<ul>
<li>查看所有数据</li>
<li>删除用户</li>
<li>系统配置</li>
<li>备份数据库</li>
</ul>
</div>
<div v-else-if="userRole === 'admin'" class="admin-content">
<h4>🔧 管理员权限</h4>
<ul>
<li>查看所有数据</li>
<li>编辑用户信息</li>
<li>审核内容</li>
</ul>
</div>
<div v-else-if="userRole === 'user'" class="user-content">
<h4>👤 普通用户权限</h4>
<ul>
<li>查看个人信息</li>
<li>编辑个人资料</li>
<li>发布内容</li>
</ul>
</div>
<div v-else class="guest-content">
<h4>👀 访客权限</h4>
<ul>
<li>浏览公开内容</li>
<li>注册账户</li>
</ul>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const score = ref('')
const userRole = ref('guest')
</script>
<style>
.if-else-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.score-input {
margin-bottom: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
.score-input label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
.score-input input {
width: 100%;
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.grade {
padding: 20px;
border-radius: 8px;
margin: 10px 0;
text-align: center;
}
.excellent {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.good {
background-color: #cce7ff;
border: 1px solid #b8daff;
color: #004085;
}
.average {
background-color: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
}
.poor {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.fail {
background-color: #f2dede;
border: 1px solid #ebcccc;
color: #a94442;
}
.empty {
background-color: #e2e3e5;
border: 1px solid #d6d8db;
color: #383d41;
}
.permission-demo {
margin-top: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.permission-demo select {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.permission-content ul {
text-align: left;
margin-top: 15px;
}
.permission-content li {
margin: 8px 0;
padding-left: 20px;
position: relative;
}
.permission-content li::before {
content: "•";
position: absolute;
left: 0;
color: #007bff;
}
</style>
v-for 列表渲染
基础用法
vue
<template>
<div class="vfor-demo">
<h2>v-for 列表渲染</h2>
<!-- 遍历数组 -->
<div class="array-demo">
<h3>数组遍历</h3>
<ul>
<li v-for="(item, index) in fruits" :key="item.id">
{{ index + 1 }}. {{ item.name }} - ¥{{ item.price }}
</li>
</ul>
</div>
<!-- 遍历对象 -->
<div class="object-demo">
<h3>对象遍历</h3>
<div class="user-info">
<div
v-for="(value, key, index) in userInfo"
:key="key"
class="info-item"
>
<span class="key">{{ key }}:</span>
<span class="value">{{ value }}</span>
</div>
</div>
</div>
<!-- 遍历数字 -->
<div class="number-demo">
<h3>数字遍历</h3>
<div class="stars">
<span
v-for="n in 5"
:key="n"
:class="{ filled: n <= rating }"
@click="setRating(n)"
>
⭐
</span>
<span class="rating-text">({{ rating }}/5)</span>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const fruits = ref([
{ id: 1, name: '苹果', price: 5.5 },
{ id: 2, name: '香蕉', price: 3.2 },
{ id: 3, name: '橙子', price: 4.8 },
{ id: 4, name: '葡萄', price: 8.0 }
])
const userInfo = ref({
name: 'Alice',
age: 25,
city: '北京',
email: 'alice@example.com',
phone: '138****1234'
})
const rating = ref(3)
const setRating = (newRating) => {
rating.value = newRating
}
</script>
<style>
.vfor-demo {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.array-demo, .object-demo, .number-demo {
margin-bottom: 30px;
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.array-demo ul {
list-style-type: none;
padding: 0;
}
.array-demo li {
padding: 10px;
margin: 5px 0;
background-color: #f8f9fa;
border-radius: 4px;
}
.user-info {
display: grid;
gap: 10px;
}
.info-item {
padding: 10px;
background-color: #e3f2fd;
border-radius: 4px;
display: flex;
justify-content: space-between;
}
.key {
font-weight: bold;
color: #1976d2;
}
.value {
color: #333;
}
.stars {
font-size: 24px;
cursor: pointer;
}
.stars span {
transition: all 0.2s ease;
}
.stars span:hover {
transform: scale(1.2);
}
.stars .filled {
color: #ffc107;
}
.rating-text {
font-size: 16px;
margin-left: 10px;
color: #666;
}
</style>
v-for 与 v-if 的组合使用
vue
<template>
<div class="vfor-if-demo">
<h2>v-for 与 v-if 组合使用</h2>
<!-- 搜索功能 -->
<div class="search-section">
<input
v-model="searchTerm"
placeholder="搜索商品..."
class="search-input"
>
<select v-model="categoryFilter" class="category-select">
<option value="">所有分类</option>
<option value="水果">水果</option>
<option value="蔬菜">蔬菜</option>
<option value="肉类">肉类</option>
</select>
</div>
<!-- 商品列表 -->
<div class="products-section">
<h3>商品列表 ({{ filteredProducts.length }} 个结果)</h3>
<!-- 方法1:在 v-for 内部使用 v-if(过滤显示) -->
<div class="product-grid">
<div
v-for="product in products"
:key="product.id"
class="product-card"
>
<div v-if="shouldShowProduct(product)" class="product-card">
<div class="product-image">
<span> {{product.image}} </span>
</div>
<div class="product-info">
<h4>{{ product.name }}</h4>
<p class="category">{{ product.category }}</p>
<p class="price">¥{{ product.price }}</p>
<button
:class="{ 'in-cart': isInCart(product.id) }"
@click="toggleCart(product.id)"
>
{{ isInCart(product.id) ? '.removeFromCart' : 'addToCart' }}
</button>
</div>
</div>
</div>
</div>
<!-- 方法2:使用计算属性预先过滤(推荐) -->
<div class="product-grid">
<div
v-for="product in filteredProducts"
:key="product.id"
class="product-card recommended"
>
<div class="product-image">
<!-- <img :src="product.image" :alt="product.name"> -->
<span> {{product.image}} </span>
</div>
<div class="product-info">
<h4>{{ product.name }}</h4>
<p class="category">{{ product.category }}</p>
<p class="price">¥{{ product.price }}</p>
<button
:class="{ 'in-cart': isInCart(product.id) }"
@click="toggleCart(product.id)"
>
{{ isInCart(product.id) ? 'removeFromCart' : 'addToCart' }}
</button>
</div>
</div>
</div>
</div>
<!-- 购物车 -->
<div class="cart-section">
<h3>购物车 ({{ cart.length }} 项)</h3>
<div v-if="cart.length === 0" class="empty-cart">
购物车为空
</div>
<div v-else class="cart-items">
<div
v-for="item in cartItems"
:key="item.id"
class="cart-item"
>
<span>{{ item.name }} x {{ item.quantity }}</span>
<span>¥{{ item.totalPrice }}</span>
</div>
<div class="cart-total">
总计: ¥{{ cartTotal }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const searchTerm = ref('')
const categoryFilter = ref('')
const products = ref([
{ id: 1, name: '红苹果', category: '水果', price: 5.5, image: '🍎' },
{ id: 2, name: '香蕉', category: '水果', price: 3.2, image: '🍌' },
{ id: 3, name: '橙子', category: '水果', price: 4.8, image: '🍊' },
{ id: 4, name: '西红柿', category: '蔬菜', price: 2.5, image: '🍅' },
{ id: 5, name: '胡萝卜', category: '蔬菜', price: 1.8, image: '🥕' },
{ id: 6, name: '牛肉', category: '肉类', price: 45.0, image: '🥩' },
{ id: 7, name: '鸡肉', category: '肉类', price: 25.0, image: '🍗' }
])
const cart = ref([])
// 计算属性:过滤后的商品
const filteredProducts = computed(() => {
return products.value.filter(product => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.value.toLowerCase())
const matchesCategory = !categoryFilter.value || product.category === categoryFilter.value
return matchesSearch && matchesCategory
})
})
// 方法:判断是否应该显示商品
const shouldShowProduct = (product) => {
const matchesSearch = product.name.toLowerCase().includes(searchTerm.value.toLowerCase())
const matchesCategory = !categoryFilter.value || product.category === categoryFilter.value
return matchesSearch && matchesCategory
}
// 购物车操作
const isInCart = (productId) => {
return cart.value.some(item => item.id === productId)
}
const toggleCart = (productId) => {
const index = cart.value.findIndex(item => item.id === productId)
if (index > -1) {
cart.value.splice(index, 1)
} else {
const product = products.value.find(p => p.id === productId)
if (product) {
cart.value.push({ ...product, quantity: 1 })
}
}
}
// 计算购物车总价
const cartItems = computed(() => {
return cart.value.map(item => ({
...item,
totalPrice: (item.price * item.quantity).toFixed(2)
}))
})
const cartTotal = computed(() => {
return cart.value.reduce((total, item) => total + (item.price * item.quantity), 0).toFixed(2)
})
</script>
<style>
.vfor-if-demo {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.search-section {
display: flex;
gap: 15px;
margin-bottom: 30px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
}
.search-input, .category-select {
padding: 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 16px;
}
.search-input {
flex: 1;
}
.products-section {
margin-bottom: 30px;
}
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
margin-top: 20px;
}
.product-card {
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.product-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.product-card.recommended {
border-color: #28a745;
}
.product-image {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background-color: #f8f9fa;
font-size: 48px;
}
.product-info {
padding: 15px;
}
.product-info h4 {
margin: 0 0 10px 0;
color: #333;
}
.category {
color: #6c757d;
font-size: 14px;
margin: 5px 0;
}
.price {
font-size: 18px;
font-weight: bold;
color: #28a745;
margin: 10px 0;
}
.product-info button {
width: 100%;
padding: 10px;
border: none;
border-radius: 4px;
background-color: #007bff;
color: white;
cursor: pointer;
transition: all 0.2s ease;
}
.product-info button:hover {
background-color: #0056b3;
}
.product-info button.in-cart {
background-color: #28a745;
}
.product-info button.in-cart:hover {
background-color: #1e7e34;
}
.cart-section {
padding: 20px;
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.empty-cart {
text-align: center;
color: #6c757d;
padding: 40px;
}
.cart-items {
margin-top: 20px;
}
.cart-item {
display: flex;
justify-content: space-between;
padding: 10px;
border-bottom: 1px solid #eee;
}
.cart-total {
font-size: 18px;
font-weight: bold;
text-align: right;
margin-top: 20px;
color: #28a745;
}
</style>
重要注意事项
1. v-for 必须使用 key
vue
<template>
<div>
<!-- ✅ 正确:使用唯一 key -->
<div v-for="item in items" :key="item.id">
{{ item.name }}
</div>
<!-- ❌ 错误:没有 key(不推荐) -->
<div v-for="item in items">
{{ item.name }}
</div>
<!-- ⚠️ 危险:使用 index 作为 key(可能导致问题) -->
<div v-for="(item, index) in items" :key="index">
<input v-model="item.name">
</div>
</div>
</template>
2. v-for 和 v-if 的优先级
vue
<template>
<div>
<!-- ❌ 不推荐:v-for 内部使用 v-if -->
<div v-for="user in users" :key="user.id" v-if="user.active">
{{ user.name }}
</div>
<!-- ✅ 推荐:使用计算属性预先过滤 -->
<div v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</div>
<!-- ✅ 或者:在容器元素上使用 v-if -->
<div v-if="showActiveUsers">
<div v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const users = ref([
{ id: 1, name: 'Alice', active: true },
{ id: 2, name: 'Bob', active: false },
{ id: 3, name: 'Charlie', active: true }
])
const showActiveUsers = ref(true)
// 推荐:使用计算属性
const activeUsers = computed(() => {
return users.value.filter(user => user.active)
})
</script>
3. 数组变更检测注意事项
vue
<template>
<div>
<h3>数组操作示例</h3>
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item }}
<button @click="removeItem(index)">删除</button>
</li>
</ul>
<div class="controls">
<input v-model="newItem" placeholder="输入新项目">
<button @click="addItem">添加</button>
<button @click="sortItems">排序</button>
<button @click="resetItems">重置</button>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref(['苹果', '香蕉', '橙子'])
const newItem = ref('')
// ✅ Vue3 可以直接修改数组索引
const removeItem = (index) => {
items.value.splice(index, 1) // 推荐使用数组方法
}
// ✅ 添加项目
const addItem = () => {
if (newItem.value.trim()) {
items.value.push(newItem.value)
newItem.value = ''
}
}
// ✅ 排序
const sortItems = () => {
items.value.sort()
}
// ✅ 重置
const resetItems = () => {
items.value = ['苹果', '香蕉', '橙子']
}
</script>
<style>
.controls {
margin-top: 20px;
padding: 15px;
background-color: #f8f9fa;
border-radius: 8px;
}
.controls input, .controls button {
margin: 5px;
padding: 8px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
}
.controls button {
background-color: #007bff;
color: white;
cursor: pointer;
border: none;
}
.controls button:hover {
background-color: #0056b3;
}
</style>
总结
使用建议
场景 | 推荐指令 | 原因 |
---|---|---|
很少切换的条件 | v-if | 初始渲染开销小 |
频繁切换的条件 | v-show | 切换开销小 |
列表渲染 | v-for | 必须配合 key |
多重条件 | v-if/v-else-if/v-else | 逻辑清晰 |
记忆口诀
- v-if:真渲染,假消失(DOM 中不存在)
- v-show:显隐藏,真假都在(CSS 控制显示)
- v-for:循环渲染,记得 key(唯一标识)
- 组合用:先过滤,再循环(性能更好)