Search K
Appearance
Appearance
📊 SEO元描述:2024年最新Vue.js列表渲染深入教程,详解v-for数组渲染、对象渲染、key重要性、数组更新检测。包含完整列表优化示例,适合开发者掌握Vue.js列表渲染技术。
核心关键词:Vue.js列表渲染、v-for、Vue数组渲染、Vue对象渲染、Vue key属性、Vue数组更新、前端列表优化
长尾关键词:v-for怎么用、Vue列表渲染优化、Vue key的作用、Vue数组更新检测、Vue.js列表性能优化
通过本节列表渲染深入,你将系统性掌握:
Vue.js列表渲染深入是什么?这是构建数据驱动界面的核心技能。Vue.js列表渲染不仅包括基本的v-for使用,还涉及性能优化、复杂数据结构、动态更新等高级应用,也是企业级Vue应用的重要组成部分。
💡 设计理念:Vue.js列表渲染的设计目标是提供高效、灵活的数据展示机制,让开发者能够轻松处理各种复杂的列表场景。
数组渲染是Vue.js中最常用的列表渲染方式,支持各种复杂的数据操作:
<template>
<div class="array-rendering-demo">
<h2>数组渲染高级应用</h2>
<!-- 基础数组渲染 -->
<div class="basic-array">
<h3>基础数组渲染</h3>
<!-- 简单数组 -->
<div class="simple-array">
<h4>简单数组</h4>
<ul>
<li v-for="(fruit, index) in fruits" :key="index">
{{ index + 1 }}. {{ fruit }}
</li>
</ul>
</div>
<!-- 对象数组 -->
<div class="object-array">
<h4>对象数组</h4>
<div class="user-grid">
<div
v-for="user in users"
:key="user.id"
class="user-card"
:class="{ active: user.isActive, premium: user.isPremium }"
>
<div class="user-avatar">
<img :src="user.avatar" :alt="user.name">
<span v-if="user.isOnline" class="online-indicator"></span>
</div>
<div class="user-info">
<h5>{{ user.name }}</h5>
<p>{{ user.email }}</p>
<div class="user-tags">
<span
v-for="tag in user.tags"
:key="tag"
class="tag"
:class="tag.toLowerCase()"
>
{{ tag }}
</span>
</div>
<div class="user-stats">
<span>文章: {{ user.posts }}</span>
<span>粉丝: {{ user.followers }}</span>
</div>
</div>
<div class="user-actions">
<button @click="toggleUserStatus(user)" class="btn-small">
{{ user.isActive ? '禁用' : '启用' }}
</button>
<button @click="editUser(user)" class="btn-small">编辑</button>
</div>
</div>
</div>
</div>
</div>
<!-- 嵌套数组渲染 -->
<div class="nested-array">
<h3>嵌套数组渲染</h3>
<div class="category-list">
<div
v-for="category in categories"
:key="category.id"
class="category-section"
>
<div class="category-header" @click="toggleCategory(category)">
<h4>
<span class="toggle-icon" :class="{ expanded: category.expanded }">▶</span>
{{ category.name }}
<span class="item-count">({{ category.items.length }})</span>
</h4>
</div>
<transition name="slide">
<div v-if="category.expanded" class="category-content">
<div
v-for="item in category.items"
:key="item.id"
class="category-item"
:class="{ featured: item.isFeatured }"
>
<div class="item-image">
<img :src="item.image" :alt="item.name">
<span v-if="item.isFeatured" class="featured-badge">精选</span>
</div>
<div class="item-details">
<h5>{{ item.name }}</h5>
<p>{{ item.description }}</p>
<div class="item-meta">
<span class="price">{{ item.price }}</span>
<span class="rating">
<span v-for="star in 5" :key="star" class="star" :class="{ filled: star <= item.rating }">★</span>
{{ item.rating }}
</span>
</div>
<div class="item-tags">
<span
v-for="tag in item.tags"
:key="tag"
class="item-tag"
>
{{ tag }}
</span>
</div>
</div>
<div class="item-actions">
<button @click="addToCart(item)" class="btn-primary">加入购物车</button>
<button @click="toggleFavorite(item)" class="btn-secondary">
{{ item.isFavorite ? '❤️' : '🤍' }}
</button>
</div>
</div>
<div v-if="category.items.length === 0" class="empty-category">
<p>该分类暂无商品</p>
<button @click="addSampleItems(category)" class="btn-secondary">添加示例商品</button>
</div>
</div>
</transition>
</div>
</div>
</div>
<!-- 动态数组操作 -->
<div class="dynamic-array">
<h3>动态数组操作</h3>
<div class="array-controls">
<div class="control-group">
<h4>数组操作</h4>
<button @click="addRandomUser">添加用户</button>
<button @click="removeRandomUser">删除用户</button>
<button @click="shuffleUsers">随机排序</button>
<button @click="sortUsers">按名称排序</button>
<button @click="filterActiveUsers">筛选活跃用户</button>
<button @click="resetUsers">重置列表</button>
</div>
<div class="control-group">
<h4>批量操作</h4>
<button @click="selectAllUsers">全选</button>
<button @click="deselectAllUsers">取消全选</button>
<button @click="deleteSelectedUsers">删除选中</button>
<button @click="activateSelectedUsers">激活选中</button>
</div>
</div>
<div class="dynamic-list">
<div class="list-header">
<span>用户列表 ({{ filteredUsers.length }})</span>
<div class="list-actions">
<input
v-model="searchTerm"
placeholder="搜索用户..."
class="search-input"
>
<select v-model="sortBy" class="sort-select">
<option value="name">按姓名</option>
<option value="email">按邮箱</option>
<option value="posts">按文章数</option>
<option value="followers">按粉丝数</option>
</select>
</div>
</div>
<transition-group name="list" tag="div" class="user-list">
<div
v-for="user in paginatedUsers"
:key="user.id"
class="dynamic-user-item"
:class="{ selected: selectedUsers.includes(user.id) }"
>
<input
type="checkbox"
:checked="selectedUsers.includes(user.id)"
@change="toggleUserSelection(user.id)"
>
<div class="user-avatar-small">
<img :src="user.avatar" :alt="user.name">
</div>
<div class="user-details">
<div class="user-name">{{ user.name }}</div>
<div class="user-email">{{ user.email }}</div>
</div>
<div class="user-metrics">
<span>{{ user.posts }} 文章</span>
<span>{{ user.followers }} 粉丝</span>
</div>
<div class="user-status">
<span :class="['status-badge', user.isActive ? 'active' : 'inactive']">
{{ user.isActive ? '活跃' : '非活跃' }}
</span>
</div>
<div class="user-actions-small">
<button @click="editUser(user)" class="btn-icon">✏️</button>
<button @click="deleteUser(user.id)" class="btn-icon">🗑️</button>
</div>
</div>
</transition-group>
<!-- 分页控制 -->
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage <= 1"
class="btn-pagination"
>
上一页
</button>
<span class="page-info">
第 {{ currentPage }} 页,共 {{ totalPages }} 页
</span>
<button
@click="currentPage++"
:disabled="currentPage >= totalPages"
class="btn-pagination"
>
下一页
</button>
</div>
</div>
</div>
<!-- 性能优化示例 -->
<div class="performance-demo">
<h3>大数据列表性能优化</h3>
<div class="performance-controls">
<button @click="generateLargeList(1000)">生成1000条数据</button>
<button @click="generateLargeList(5000)">生成5000条数据</button>
<button @click="generateLargeList(10000)">生成10000条数据</button>
<button @click="clearLargeList">清空列表</button>
<span>当前数据量: {{ largeList.length }}</span>
</div>
<!-- 虚拟滚动示例(简化版) -->
<div class="virtual-scroll-container" ref="scrollContainer" @scroll="handleScroll">
<div class="virtual-scroll-content" :style="{ height: totalHeight + 'px' }">
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-item"
:style="{ transform: `translateY(${item.top}px)` }"
>
<div class="item-content">
<span class="item-id">#{{ item.id }}</span>
<span class="item-name">{{ item.name }}</span>
<span class="item-value">{{ item.value }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'ArrayRenderingDemo',
data() {
return {
fruits: ['苹果', '香蕉', '橙子', '葡萄', '草莓'],
users: [
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
avatar: 'https://via.placeholder.com/50',
isActive: true,
isPremium: true,
isOnline: true,
tags: ['VIP', 'Developer'],
posts: 25,
followers: 150
},
{
id: 2,
name: '李四',
email: 'lisi@example.com',
avatar: 'https://via.placeholder.com/50',
isActive: false,
isPremium: false,
isOnline: false,
tags: ['User'],
posts: 8,
followers: 45
}
],
categories: [
{
id: 1,
name: '电子产品',
expanded: true,
items: [
{
id: 1,
name: 'iPhone 15',
description: '最新款苹果手机',
price: '¥7999',
rating: 5,
image: 'https://via.placeholder.com/100',
isFeatured: true,
isFavorite: false,
tags: ['热门', '新品']
}
]
},
{
id: 2,
name: '服装',
expanded: false,
items: []
}
],
searchTerm: '',
sortBy: 'name',
selectedUsers: [],
currentPage: 1,
pageSize: 5,
largeList: [],
scrollTop: 0,
itemHeight: 60,
containerHeight: 300
}
},
computed: {
filteredUsers() {
let filtered = this.users
// 搜索过滤
if (this.searchTerm) {
filtered = filtered.filter(user =>
user.name.toLowerCase().includes(this.searchTerm.toLowerCase()) ||
user.email.toLowerCase().includes(this.searchTerm.toLowerCase())
)
}
// 排序
filtered.sort((a, b) => {
const aVal = a[this.sortBy]
const bVal = b[this.sortBy]
if (typeof aVal === 'string') {
return aVal.localeCompare(bVal)
}
return bVal - aVal
})
return filtered
},
totalPages() {
return Math.ceil(this.filteredUsers.length / this.pageSize)
},
paginatedUsers() {
const start = (this.currentPage - 1) * this.pageSize
const end = start + this.pageSize
return this.filteredUsers.slice(start, end)
},
// 虚拟滚动相关计算属性
totalHeight() {
return this.largeList.length * this.itemHeight
},
visibleStart() {
return Math.floor(this.scrollTop / this.itemHeight)
},
visibleEnd() {
return Math.min(
this.visibleStart + Math.ceil(this.containerHeight / this.itemHeight) + 1,
this.largeList.length
)
},
visibleItems() {
return this.largeList.slice(this.visibleStart, this.visibleEnd).map((item, index) => ({
...item,
top: (this.visibleStart + index) * this.itemHeight
}))
}
},
methods: {
toggleUserStatus(user) {
user.isActive = !user.isActive
},
editUser(user) {
alert(`编辑用户: ${user.name}`)
},
toggleCategory(category) {
category.expanded = !category.expanded
},
addToCart(item) {
alert(`已添加到购物车: ${item.name}`)
},
toggleFavorite(item) {
item.isFavorite = !item.isFavorite
},
addSampleItems(category) {
const sampleItems = [
{
id: Date.now(),
name: '示例商品',
description: '这是一个示例商品',
price: '¥99',
rating: 4,
image: 'https://via.placeholder.com/100',
isFeatured: false,
isFavorite: false,
tags: ['示例']
}
]
category.items.push(...sampleItems)
},
addRandomUser() {
const names = ['王五', '赵六', '孙七', '周八', '吴九']
const randomName = names[Math.floor(Math.random() * names.length)]
const newUser = {
id: Date.now(),
name: randomName,
email: `${randomName.toLowerCase()}@example.com`,
avatar: 'https://via.placeholder.com/50',
isActive: Math.random() > 0.5,
isPremium: Math.random() > 0.7,
isOnline: Math.random() > 0.5,
tags: ['User'],
posts: Math.floor(Math.random() * 50),
followers: Math.floor(Math.random() * 200)
}
this.users.push(newUser)
},
removeRandomUser() {
if (this.users.length > 0) {
const randomIndex = Math.floor(Math.random() * this.users.length)
this.users.splice(randomIndex, 1)
}
},
shuffleUsers() {
for (let i = this.users.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[this.users[i], this.users[j]] = [this.users[j], this.users[i]]
}
},
sortUsers() {
this.users.sort((a, b) => a.name.localeCompare(b.name))
},
filterActiveUsers() {
this.users = this.users.filter(user => user.isActive)
},
resetUsers() {
// 重置为初始数据
this.users = [
{
id: 1,
name: '张三',
email: 'zhangsan@example.com',
avatar: 'https://via.placeholder.com/50',
isActive: true,
isPremium: true,
isOnline: true,
tags: ['VIP', 'Developer'],
posts: 25,
followers: 150
}
]
},
selectAllUsers() {
this.selectedUsers = this.filteredUsers.map(user => user.id)
},
deselectAllUsers() {
this.selectedUsers = []
},
toggleUserSelection(userId) {
const index = this.selectedUsers.indexOf(userId)
if (index > -1) {
this.selectedUsers.splice(index, 1)
} else {
this.selectedUsers.push(userId)
}
},
deleteSelectedUsers() {
this.users = this.users.filter(user => !this.selectedUsers.includes(user.id))
this.selectedUsers = []
},
activateSelectedUsers() {
this.users.forEach(user => {
if (this.selectedUsers.includes(user.id)) {
user.isActive = true
}
})
},
deleteUser(userId) {
this.users = this.users.filter(user => user.id !== userId)
},
generateLargeList(count) {
this.largeList = Array.from({ length: count }, (_, index) => ({
id: index + 1,
name: `项目 ${index + 1}`,
value: Math.floor(Math.random() * 1000)
}))
},
clearLargeList() {
this.largeList = []
},
handleScroll(event) {
this.scrollTop = event.target.scrollTop
}
}
}
</script>
<style scoped>
.array-rendering-demo {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.basic-array,
.nested-array,
.dynamic-array,
.performance-demo {
margin: 30px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background-color: #fafafa;
}
.user-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 20px;
margin: 20px 0;
}
.user-card {
background-color: white;
border-radius: 8px;
padding: 15px;
border: 1px solid #ddd;
transition: all 0.3s ease;
}
.user-card.active {
border-color: #42b983;
}
.user-card.premium {
background: linear-gradient(135deg, #fff9e6, #fff);
border-color: #ffd700;
}
.user-avatar {
position: relative;
width: 50px;
height: 50px;
margin-bottom: 10px;
}
.user-avatar img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.online-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
background-color: #4caf50;
border-radius: 50%;
border: 2px solid white;
}
.user-tags {
display: flex;
gap: 5px;
margin: 8px 0;
}
.tag {
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.tag.vip {
background-color: #ffd700;
color: #333;
}
.tag.developer {
background-color: #42b983;
color: white;
}
.tag.user {
background-color: #6c757d;
color: white;
}
.category-section {
margin: 15px 0;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.category-header {
background-color: #f8f9fa;
padding: 15px;
cursor: pointer;
border-bottom: 1px solid #ddd;
}
.category-header:hover {
background-color: #e9ecef;
}
.toggle-icon {
display: inline-block;
transition: transform 0.3s ease;
margin-right: 8px;
}
.toggle-icon.expanded {
transform: rotate(90deg);
}
.category-content {
padding: 15px;
}
.category-item {
display: flex;
gap: 15px;
padding: 15px;
margin: 10px 0;
background-color: white;
border-radius: 8px;
border: 1px solid #ddd;
}
.category-item.featured {
border-color: #ffd700;
background: linear-gradient(135deg, #fff9e6, #fff);
}
.item-image {
position: relative;
width: 100px;
height: 100px;
}
.item-image img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 4px;
}
.featured-badge {
position: absolute;
top: -5px;
right: -5px;
background-color: #ff4757;
color: white;
padding: 2px 6px;
border-radius: 8px;
font-size: 10px;
font-weight: bold;
}
.item-details {
flex: 1;
}
.item-meta {
display: flex;
gap: 15px;
align-items: center;
margin: 8px 0;
}
.price {
font-size: 18px;
font-weight: bold;
color: #e74c3c;
}
.rating {
display: flex;
align-items: center;
gap: 5px;
}
.star {
color: #ddd;
}
.star.filled {
color: #ffd700;
}
.item-tags {
display: flex;
gap: 5px;
margin: 8px 0;
}
.item-tag {
background-color: #f8f9fa;
padding: 2px 8px;
border-radius: 12px;
font-size: 12px;
border: 1px solid #dee2e6;
}
.array-controls {
display: flex;
gap: 30px;
margin: 20px 0;
flex-wrap: wrap;
}
.control-group {
background-color: white;
padding: 15px;
border-radius: 8px;
border: 1px solid #ddd;
}
.control-group h4 {
margin: 0 0 10px 0;
color: #333;
}
.control-group button {
margin: 3px;
padding: 6px 12px;
border: 1px solid #42b983;
background-color: #42b983;
color: white;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background-color: white;
border-radius: 8px;
margin-bottom: 15px;
}
.list-actions {
display: flex;
gap: 10px;
align-items: center;
}
.search-input,
.sort-select {
padding: 6px 10px;
border: 1px solid #ddd;
border-radius: 4px;
}
.user-list {
background-color: white;
border-radius: 8px;
overflow: hidden;
}
.dynamic-user-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 15px;
border-bottom: 1px solid #eee;
transition: all 0.3s ease;
}
.dynamic-user-item:hover {
background-color: #f8f9fa;
}
.dynamic-user-item.selected {
background-color: #e3f2fd;
}
.user-avatar-small {
width: 40px;
height: 40px;
}
.user-avatar-small img {
width: 100%;
height: 100%;
border-radius: 50%;
object-fit: cover;
}
.user-details {
flex: 1;
}
.user-name {
font-weight: bold;
color: #333;
}
.user-email {
color: #666;
font-size: 14px;
}
.user-metrics {
display: flex;
gap: 15px;
font-size: 14px;
color: #666;
}
.status-badge {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.status-badge.active {
background-color: #d4edda;
color: #155724;
}
.status-badge.inactive {
background-color: #f8d7da;
color: #721c24;
}
.user-actions-small {
display: flex;
gap: 5px;
}
.btn-icon {
background: none;
border: none;
cursor: pointer;
padding: 5px;
border-radius: 4px;
}
.btn-icon:hover {
background-color: #f8f9fa;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
gap: 15px;
margin-top: 20px;
padding: 15px;
background-color: white;
border-radius: 8px;
}
.btn-pagination {
padding: 8px 16px;
border: 1px solid #42b983;
background-color: #42b983;
color: white;
border-radius: 4px;
cursor: pointer;
}
.btn-pagination:disabled {
background-color: #ccc;
border-color: #ccc;
cursor: not-allowed;
}
.virtual-scroll-container {
height: 300px;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 8px;
background-color: white;
}
.virtual-scroll-content {
position: relative;
}
.virtual-item {
position: absolute;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
padding: 0 15px;
border-bottom: 1px solid #eee;
}
.item-content {
display: flex;
gap: 20px;
align-items: center;
width: 100%;
}
.item-id {
font-weight: bold;
color: #666;
width: 60px;
}
.item-name {
flex: 1;
color: #333;
}
.item-value {
color: #42b983;
font-weight: bold;
}
/* 动画效果 */
.slide-enter-active,
.slide-leave-active {
transition: all 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
opacity: 0;
transform: translateY(-10px);
}
.list-enter-active,
.list-leave-active {
transition: all 0.3s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-move {
transition: transform 0.3s ease;
}
.btn-small,
.btn-primary,
.btn-secondary {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin: 2px;
}
.btn-small {
background-color: #6c757d;
color: white;
}
.btn-primary {
background-color: #007bff;
color: white;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.empty-category {
text-align: center;
padding: 30px;
color: #666;
}
</style>