深入理解Vue3条件渲染机制与列表渲染优化,掌握性能调优技巧
条件渲染是Vue中控制元素显示/隐藏的核心功能,v-if和v-show虽然功能相似,但实现机制和适用场景完全不同。
v-if:条件性渲染 vs v-show:CSS显示控制
display: blockdisplay: nonev-if指令用于真正的条件渲染,条件为真时元素才会被渲染到DOM中。
<template>
<div>
<h1 v-if="awesome">Vue is awesome!</h1>
<h1 v-else>Oh no</h1>
<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>
</div>
</template>
<script setup>
import { ref } from 'vue'
const awesome = ref(true)
const type = ref('A')
</script>
使用<template>标签可以在不添加额外DOM元素的情况下条件渲染多个元素。
<template>
<div>
<!-- 使用template包裹多个元素 -->
<template v-if="userLoggedIn">
<h1>欢迎回来,{{ userName }}!</h1>
<nav>
<a href="/profile">个人资料</a>
<a href="/settings">设置</a>
<button @click="logout">退出登录</button>
</nav>
</template>
<!-- 未登录状态 -->
<template v-else>
<h1>请先登录</h1>
<button @click="login">登录</button>
<button @click="register">注册</button>
</template>
<!-- 复杂条件判断 -->
<template v-if="loading">
<div class="loading">加载中...</div>
</template>
<template v-else-if="error">
<div class="error">{{ errorMessage }}</div>
</template>
<template v-else>
<div class="content">内容加载完成</div>
</template>
</div>
</template>
<script setup>
import { ref } from 'vue'
const userLoggedIn = ref(true)
const userName = ref('张三')
const loading = ref(false)
const error = ref(false)
const errorMessage = ref('')
function login() {
userLoggedIn.value = true
userName.value = '新用户'
}
function logout() {
userLoggedIn.value = false
userName.value = ''
}
function register() {
console.log('注册功能')
}
</script>
v-show通过CSS的display属性控制元素的显示和隐藏,元素始终存在于DOM中。
<template>
<div>
<h1 v-show="isVisible">Hello Vue3!</h1>
<div style="margin: 20px 0;">
<button @click="toggleVisibility">
{{ isVisible ? '隐藏' : '显示' }}标题
</button>
<button @click="toggleAnimation">
{{ useAnimation ? '关闭' : '开启' }}动画
</button>
</div>
<!-- 复杂组件的显示控制 -->
<div v-show="showAdvancedOptions" class="advanced-options">
<h3>高级选项</h3>
<div>
<label>选项1: <input type="checkbox" v-model="option1"></label>
<label>选项2: <input type="checkbox" v-model="option2"></label>
<label>选项3: <input type="checkbox" v-model="option3"></label>
</div>
</div>
<button @click="showAdvancedOptions = !showAdvancedOptions">
{{ showAdvancedOptions ? '隐藏' : '显示' }}高级选项
</button>
<!-- 显示当前状态 -->
<p>标题显示状态: {{ isVisible ? '显示' : '隐藏' }}</p>
<p>高级选项状态: {{ showAdvancedOptions ? '显示' : '隐藏' }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const isVisible = ref(true)
const useAnimation = ref(true)
const showAdvancedOptions = ref(false)
const option1 = ref(false)
const option2 = ref(false)
const option3 = ref(false)
function toggleVisibility() {
isVisible.value = !isVisible.value
}
function toggleAnimation() {
useAnimation.value = !useAnimation.value
}
</script>
<style scoped>
.advanced-options {
border: 1px solid #ccc;
padding: 15px;
margin: 10px 0;
border-radius: 5px;
background-color: #f9f9f9;
}
h1 {
transition: all 0.3s ease;
}
h1[v-show="false"] {
opacity: 0;
transform: translateY(-20px);
}
</style>
| 特性 | v-if | v-show |
|---|---|---|
| 渲染机制 | 条件性渲染,元素创建/销毁 | CSS显示控制,元素始终存在 |
| 初始渲染开销 | 低(条件为假时不渲染) | 高(始终渲染到DOM) |
| 切换开销 | 高(重新创建/销毁) | 低(只修改CSS) |
| 组件生命周期 | 触发创建/销毁钩子 | 不触发生命周期变化 |
| DOM结构 | 条件为假时元素不存在 | 元素始终在DOM中 |
| 适用场景 | 运行时条件很少改变 | 频繁切换显示状态 |
<KeepAlive>使用v-for指令是Vue中最强大的功能之一,用于基于数组、对象或数字范围渲染列表。
虚拟DOM的列表diff算法
<template>
<div>
<h3>任务列表管理</h3>
<!-- 基本遍历 -->
<div class="task-list">
<h4>任务列表(基本遍历)</h4>
<ul>
<li v-for="task in tasks" :key="task.id">
<span :class="{ completed: task.completed }">{{ task.title }}</span>
<button @click="toggleTask(task.id)">{{ task.completed ? '未完成' : '完成' }}</button>
<button @click="removeTask(task.id)">删除</button>
</li>
</ul>
</div>
<!-- 带索引遍历 -->
<div class="task-list">
<h4>任务列表(带索引)</h4>
<ul>
<li v-for="(task, index) in tasks" :key="task.id">
<span>{{ index + 1 }}. {{ task.title }}</span>
<span :class="['priority', 'priority-' + task.priority]">
优先级: {{ getPriorityText(task.priority) }}
</span>
</li>
</ul>
</div>
<!-- 条件渲染与列表结合 -->
<div class="task-list">
<h4>已完成任务</h4>
<template v-if="completedTasks.length > 0">
<ul>
<li v-for="task in completedTasks" :key="task.id">
✅ {{ task.title }}
</li>
</ul>
</template>
<p v-else>暂无已完成任务</p>
</div>
<!-- 统计信息 -->
<div class="stats">
<p>总任务数: {{ totalTasks }}</p>
<p>已完成: {{ completedTasksCount }}</p>
<p>未完成: {{ pendingTasksCount }}</p>
<p>完成率: {{ completionRate }}%</p>
</div>
<!-- 添加新任务 -->
<div class="add-task">
<input v-model="newTaskTitle" placeholder="输入新任务">
<select v-model="newTaskPriority">
<option value="1">低优先级</option>
<option value="2">中优先级</option>
<option value="3">高优先级</option>
</select>
<button @click="addTask">添加任务</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 任务数据
const tasks = ref([
{ id: 1, title: '学习Vue3基础', completed: true, priority: 3 },
{ id: 2, title: '掌握组件开发', completed: false, priority: 2 },
{ id: 3, title: '学习路由管理', completed: false, priority: 2 },
{ id: 4, title: '状态管理实践', completed: false, priority: 1 }
])
const newTaskTitle = ref('')
const newTaskPriority = ref('2')
// 计算属性
const completedTasks = computed(() => tasks.value.filter(task => task.completed))
const totalTasks = computed(() => tasks.value.length)
const completedTasksCount = computed(() => completedTasks.value.length)
const pendingTasksCount = computed(() => totalTasks.value - completedTasksCount.value)
const completionRate = computed(() => {
return totalTasks.value > 0 ? Math.round((completedTasksCount.value / totalTasks.value) * 100) : 0
})
// 方法
function addTask() {
if (newTaskTitle.value.trim()) {
tasks.value.push({
id: Date.now(),
title: newTaskTitle.value,
completed: false,
priority: parseInt(newTaskPriority.value)
})
newTaskTitle.value = ''
}
}
function toggleTask(id) {
const task = tasks.value.find(t => t.id === id)
if (task) task.completed = !task.completed
}
function removeTask(id) {
tasks.value = tasks.value.filter(t => t.id !== id)
}
function getPriorityText(priority) {
const priorityMap = { 1: '低', 2: '中', 3: '高' }
return priorityMap[priority] || '未知'
}
</script>
<style scoped>
.task-list {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.completed {
text-decoration: line-through;
color: #888;
}
.priority { padding: 2px 8px; border-radius: 3px; color: white; }
.priority-1 { background: #28a745; }
.priority-2 { background: #ffc107; color: black; }
.priority-3 { background: #dc3545; }
.stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 20px 0;
}
.add-task {
display: flex;
gap: 10px;
align-items: center;
}
</style>
<template>
<div>
<h3>用户信息展示</h3>
<!-- 基本遍历:只获取值 -->
<div class="info-section">
<h4>用户信息(值遍历)</h4>
<ul>
<li v-for="value in userInfo" :key="value">
{{ value }}
</li>
</ul>
</div>
<!-- 带键名遍历 -->
<div class="info-section">
<h4>用户信息(键值对)</h4>
<ul>
<li v-for="(value, key) in userInfo" :key="key">
<strong>{{ formatKey(key) }}:</strong> {{ formatValue(key, value) }}
</li>
</ul>
</div>
<!-- 带键名和索引 -->
<div class="info-section">
<h4>用户信息(带索引)</h4>
<table>
<thead>
<tr>
<th>序号</th>
<th>属性名</th>
<th>属性值</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key, index) in userInfo" :key="key">
<td>{{ index + 1 }}</td>
<td>{{ formatKey(key) }}</td>
<td>{{ formatValue(key, value) }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 嵌套对象遍历 -->
<div class="info-section" v-if="userProfile">
<h4>用户档案(嵌套对象)</h4>
<div v-for="(section, sectionName) in userProfile" :key="sectionName">
<h5>{{ formatSectionName(sectionName) }}</h5>
<ul>
<li v-for="(value, key) in section" :key="key">
{{ formatKey(key) }}: {{ value }}
</li>
</ul>
</div>
</div>
<!-- 编辑模式 -->
<div class="info-section">
<h4>编辑用户信息</h4>
<div v-for="(value, key) in editableUserInfo" :key="key" class="edit-field">
<label :for="key">{{ formatKey(key) }}:</label>
<input :id="key" v-model="editableUserInfo[key]" :type="getInputType(key)">
</div>
<button @click="saveChanges">保存修改</button>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'
// 用户信息对象
const userInfo = reactive({
name: '张三',
age: 25,
email: 'zhangsan@example.com',
phone: '13800138000',
city: '北京市',
occupation: '前端工程师',
joinDate: '2023-01-15'
})
// 嵌套用户档案
const userProfile = reactive({
basic: {
gender: '男',
birthday: '1998-05-20',
education: '本科'
},
work: {
company: '某科技公司',
department: '前端开发部',
position: '高级工程师'
},
contact: {
wechat: 'zhangsan123',
qq: '123456789',
address: '北京市朝阳区'
}
})
// 可编辑的用户信息
const editableUserInfo = reactive({ ...userInfo })
// 格式化方法
function formatKey(key) {
const keyMap = {
name: '姓名', age: '年龄', email: '邮箱', phone: '电话',
city: '城市', occupation: '职业', joinDate: '加入日期',
gender: '性别', birthday: '生日', education: '学历',
company: '公司', department: '部门', position: '职位',
wechat: '微信', qq: 'QQ', address: '地址'
}
return keyMap[key] || key
}
function formatValue(key, value) {
if (key === 'joinDate' || key === 'birthday') {
return new Date(value).toLocaleDateString('zh-CN')
}
return value
}
function formatSectionName(sectionName) {
const sectionMap = {
basic: '基本信息', work: '工作信息', contact: '联系信息'
}
return sectionMap[sectionName] || sectionName
}
function getInputType(key) {
if (key === 'age') return 'number'
if (key.includes('Date') || key === 'birthday') return 'date'
if (key === 'email') return 'email'
if (key === 'phone') return 'tel'
return 'text'
}
function saveChanges() {
Object.assign(userInfo, editableUserInfo)
alert('用户信息已更新!')
}
</script>
<style scoped>
.info-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
table {
width: 100%;
border-collapse: collapse;
}
table th, table td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
.edit-field {
margin: 10px 0;
display: flex;
align-items: center;
}
.edit-field label {
width: 100px;
font-weight: bold;
}
.edit-field input {
flex: 1;
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
}
</style>
<template>
<div>
<h3>数字范围遍历应用</h3>
<!-- 基本数字遍历 -->
<div class="number-section">
<h4>1-10的数字</h4>
<div class="number-grid">
<span v-for="n in 10" :key="n" class="number-item">{{ n }}</span>
</div>
</div>
<!-- 动态范围 -->
<div class="number-section">
<h4>自定义范围</h4>
<div>
<label>起始值: <input v-model.number="start" type="number"></label>
<label>结束值: <input v-model.number="end" type="number"></label>
</div>
<div class="number-grid">
<span v-for="n in range" :key="n" class="number-item">{{ n }}</span>
</div>
</div>
<!-- 生成表格 -->
<div class="number-section">
<h4>乘法表生成器</h4>
<table class="multiplication-table">
<tr>
<th>×</th>
<th v-for="col in tableSize" :key="col">{{ col }}</th>
</tr>
<tr v-for="row in tableSize" :key="row">
<th>{{ row }}</th>
<td v-for="col in tableSize" :key="col">{{ row * col }}</td>
</tr>
</table>
</div>
<!-- 分页器 -->
<div class="number-section">
<h4>分页导航</h4>
<div class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一页</button>
<span v-for="page in totalPages" :key="page"
:class="{ active: page === currentPage }"
@click="goToPage(page)">
{{ page }}
</span>
<button @click="nextPage" :disabled="currentPage === totalPages">下一页</button>
</div>
<p>当前页: {{ currentPage }} / {{ totalPages }}</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const start = ref(1)
const end = ref(10)
const tableSize = ref(9)
const currentPage = ref(1)
const totalPages = ref(10)
// 计算属性
const range = computed(() => {
const result = []
for (let i = start.value; i <= end.value; i++) {
result.push(i)
}
return result
})
// 方法
function prevPage() {
if (currentPage.value > 1) currentPage.value--
}
function nextPage() {
if (currentPage.value < totalPages.value) currentPage.value++
}
function goToPage(page) {
currentPage.value = page
}
</script>
<style scoped>
.number-section {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 5px;
}
.number-grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
gap: 5px;
margin: 10px 0;
}
.number-item {
padding: 8px;
text-align: center;
background: #f0f0f0;
border-radius: 3px;
}
.multiplication-table {
border-collapse: collapse;
margin: 10px 0;
}
.multiplication-table th,
.multiplication-table td {
border: 1px solid #ddd;
padding: 8px;
text-align: center;
min-width: 40px;
}
.multiplication-table th {
background: #f5f5f5;
font-weight: bold;
}
.pagination {
display: flex;
gap: 5px;
align-items: center;
margin: 10px 0;
}
.pagination span {
padding: 5px 10px;
border: 1px solid #ddd;
cursor: pointer;
border-radius: 3px;
}
.pagination span.active {
background: #42b883;
color: white;
border-color: #42b883;
}
.pagination button {
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 3px;
}
.pagination button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
虚拟DOM的节点复用机制
// 原始列表
<div>A</div> // 索引0
<div>B</div> // 索引1
<div>C</div> // 索引2
// 删除第一个元素后
<div>B</div> // 原索引1,现索引0(内容错误)
<div>C</div> // 原索引2,现索引1(内容错误)
// 原始列表
<div key="1">A</div>
<div key="2">B</div>
<div key="3">C</div>
// 删除第一个元素后
<div key="2">B</div> // 正确识别
<div key="3">C</div> // 正确识别
<template>
<div>
<h3>key属性性能对比</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<!-- 使用索引作为key(不推荐) -->
<div class="demo-section">
<h4>❌ 使用索引作为key</h4>
<ul>
<li v-for="(item, index) in itemsWithIndexKey" :key="index">
<input v-model="item.text" placeholder="输入内容">
<span>{{ item.text }}</span>
</li>
</ul>
<button @click="addItemWithIndexKey">添加项目</button>
<button @click="removeFirstItemWithIndexKey">删除第一个</button>
<p>问题:删除第一个项目后,输入框状态会错乱</p>
</div>
<!-- 使用唯一ID作为key(推荐) -->
<div class="demo-section">
<h4>✅ 使用唯一ID作为key</h4>
<ul>
<li v-for="item in itemsWithIdKey" :key="item.id">
<input v-model="item.text" placeholder="输入内容">
<span>{{ item.text }}</span>
</li>
</ul>
<button @click="addItemWithIdKey">添加项目</button>
<button @click="removeFirstItemWithIdKey">删除第一个</button>
<p>优势:删除项目后,其他项目的状态保持不变</p>
</div>
</div>
<div class="conclusion">
<h4>📋 结论</h4>
<ul>
<li><strong>使用索引作为key的问题:</strong>
<ul>
<li>列表重新排序时,组件状态会错乱</li>
<li>删除/添加项目时,输入框状态可能丢失</li>
<li>性能较差,无法充分利用虚拟DOM的复用机制</li>
</ul>
</li>
<li><strong>使用唯一ID作为key的优势:</strong>
<ul>
<li>组件状态保持稳定</li>
<li>性能优化,DOM复用更高效</li>
<li>列表操作更安全可靠</li>
</ul>
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 使用索引作为key的数据
const itemsWithIndexKey = ref([
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' }
])
// 使用唯一ID作为key的数据
const itemsWithIdKey = ref([
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' }
])
function addItemWithIndexKey() {
itemsWithIndexKey.value.push({
id: Date.now(),
text: '新项目'
})
}
function removeFirstItemWithIndexKey() {
itemsWithIndexKey.value.shift()
}
function addItemWithIdKey() {
itemsWithIdKey.value.push({
id: Date.now(),
text: '新项目'
})
}
function removeFirstItemWithIdKey() {
itemsWithIdKey.value.shift()
}
</script>
<style scoped>
.demo-section {
border: 2px solid #ddd;
padding: 15px;
border-radius: 5px;
}
.demo-section h4 {
margin-top: 0;
}
.demo-section ul {
list-style: none;
padding: 0;
}
.demo-section li {
margin: 10px 0;
padding: 10px;
border: 1px solid #eee;
border-radius: 3px;
display: flex;
align-items: center;
gap: 10px;
}
.demo-section input {
padding: 5px;
border: 1px solid #ccc;
border-radius: 3px;
flex: 1;
}
.conclusion {
margin-top: 20px;
padding: 15px;
background: #f8f9fa;
border-radius: 5px;
}
.conclusion ul {
margin: 10px 0;
}
.conclusion li {
margin: 5px 0;
}
</style>
<template>
<div>
<ul>
<li v-for="item in items" :key="item.id">{{ item.text }}</li>
</ul>
<button @click="addItem">添加</button>
<button @click="removeItem">删除</button>
<button @click="reverseItems">反转</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const items = ref([
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' }
])
let nextId = 3
function addItem() {
items.value.push({ id: nextId++, text: `项目${nextId - 1}` })
}
function removeItem() {
items.value.pop()
}
function reverseItems() {
items.value.reverse()
}
</script>
<template>
<div>
<!-- 不推荐:v-if和v-for在同一元素上 -->
<li v-for="user in users" v-if="user.isActive" :key="user.id">
{{ user.name }}
</li>
<!-- 推荐:使用计算属性过滤 -->
<li v-for="user in activeUsers" :key="user.id">
{{ user.name }}
</li>
<!-- 或使用template包裹 -->
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">{{ user.name }}</li>
</template>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const users = ref([
{ id: 1, name: '张三', isActive: true },
{ id: 2, name: '李四', isActive: false },
{ id: 3, name: '王五', isActive: true }
])
const activeUsers = computed(() => {
return users.value.filter(user => user.isActive)
})
</script>
<template>
<div>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加待办事项">
<button @click="addTodo">添加</button>
<ul>
<li v-for="todo in todos" :key="todo.id">
<input type="checkbox" v-model="todo.done">
<span :style="{ textDecoration: todo.done ? 'line-through' : 'none' }">
{{ todo.text }}
</span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</ul>
<p v-if="todos.length === 0">暂无待办事项</p>
<p v-else>
已完成: {{ completedCount }} / {{ todos.length }}
</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const newTodo = ref('')
const todos = ref([])
let nextId = 1
function addTodo() {
if (newTodo.value.trim()) {
todos.value.push({
id: nextId++,
text: newTodo.value,
done: false
})
newTodo.value = ''
}
}
function removeTodo(id) {
todos.value = todos.value.filter(todo => todo.id !== id)
}
const completedCount = computed(() => {
return todos.value.filter(todo => todo.done).length
})
</script>