x-for 列表渲染 #
什么是 x-for? #
x-for 指令用于基于数组或对象循环渲染元素列表。它是 Alpine.js 中最常用的指令之一,可以高效地渲染动态数据。
基本语法 #
x-for 必须在 <template> 标签上使用:
html
<template x-for="item in items">
<div x-text="item"></div>
</template>
基本用法 #
数组循环 #
html
<div x-data="{
fruits: ['Apple', 'Banana', 'Orange']
}">
<template x-for="fruit in fruits">
<p x-text="fruit"></p>
</template>
</div>
获取索引 #
html
<div x-data="{
items: ['First', 'Second', 'Third']
}">
<template x-for="(item, index) in items">
<p>
<span x-text="index + 1"></span>.
<span x-text="item"></span>
</p>
</template>
</div>
对象数组 #
html
<div x-data="{
users: [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
{ id: 3, name: 'Bob', email: 'bob@example.com' }
]
}">
<template x-for="user in users">
<div>
<h3 x-text="user.name"></h3>
<p x-text="user.email"></p>
</div>
</template>
</div>
key 属性 #
为什么需要 key? #
:key 帮助 Alpine.js 追踪每个节点的身份,在列表更新时高效地复用和重新排序元素。
html
<template x-for="item in items" :key="item.id">
<div x-text="item.name"></div>
</template>
使用唯一 ID #
html
<div x-data="{
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
]
}">
<template x-for="item in items" :key="item.id">
<div x-text="item.name"></div>
</template>
</div>
使用索引作为 key #
当没有唯一 ID 时,可以使用索引(不推荐用于会重新排序的列表):
html
<template x-for="(item, index) in items" :key="index">
<div x-text="item"></div>
</template>
循环范围 #
使用数字进行固定次数循环:
html
<div x-data>
<template x-for="i in 5">
<p x-text="'Item ' + i"></p>
</template>
</div>
输出:
text
Item 1
Item 2
Item 3
Item 4
Item 5
动态范围 #
html
<div x-data="{ count: 3 }">
<input type="number" x-model="count">
<template x-for="i in count">
<p x-text="'Item ' + i"></p>
</template>
</div>
对象循环 #
遍历对象属性 #
html
<div x-data="{
user: {
name: 'John',
email: 'john@example.com',
city: 'Beijing'
}
}">
<template x-for="(value, key) in user">
<p>
<strong x-text="key"></strong>:
<span x-text="value"></span>
</p>
</template>
</div>
获取索引 #
html
<template x-for="(value, key, index) in object">
<p x-text="`${index}. ${key}: ${value}`"></p>
</template>
嵌套循环 #
双层循环 #
html
<div x-data="{
categories: [
{
name: 'Fruits',
items: ['Apple', 'Banana']
},
{
name: 'Vegetables',
items: ['Carrot', 'Broccoli']
}
]
}">
<template x-for="category in categories" :key="category.name">
<div>
<h3 x-text="category.name"></h3>
<template x-for="item in category.items">
<p x-text="item"></p>
</template>
</div>
</template>
</div>
带索引的嵌套 #
html
<div x-data="{
matrix: [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
}">
<template x-for="(row, rowIndex) in matrix">
<div>
<template x-for="(cell, colIndex) in row">
<span x-text="`(${rowIndex},${colIndex})=${cell} `"></span>
</template>
</div>
</template>
</div>
列表操作 #
添加项目 #
html
<div x-data="{
items: ['Item 1', 'Item 2'],
newItem: '',
addItem() {
if (this.newItem.trim()) {
this.items.push(this.newItem.trim())
this.newItem = ''
}
}
}">
<input x-model="newItem" @keyup.enter="addItem()">
<button @click="addItem()">添加</button>
<ul>
<template x-for="item in items">
<li x-text="item"></li>
</template>
</ul>
</div>
删除项目 #
html
<div x-data="{
items: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' }
],
removeItem(id) {
this.items = this.items.filter(item => item.id !== id)
}
}">
<template x-for="item in items" :key="item.id">
<div>
<span x-text="item.name"></span>
<button @click="removeItem(item.id)">删除</button>
</div>
</template>
</div>
更新项目 #
html
<div x-data="{
items: [
{ id: 1, name: 'Item 1', done: false },
{ id: 2, name: 'Item 2', done: false }
],
toggleItem(id) {
const item = this.items.find(i => i.id === id)
if (item) item.done = !item.done
}
}">
<template x-for="item in items" :key="item.id">
<div>
<input
type="checkbox"
:checked="item.done"
@change="toggleItem(item.id)"
>
<span
x-text="item.name"
:style="item.done ? 'text-decoration: line-through' : ''"
></span>
</div>
</template>
</div>
排序 #
html
<div x-data="{
items: [
{ id: 3, name: 'Charlie' },
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
],
sortBy: 'id',
get sortedItems() {
return [...this.items].sort((a, b) => a[this.sortBy] - b[this.sortBy])
}
}">
<select x-model="sortBy">
<option value="id">按 ID 排序</option>
<option value="name">按名称排序</option>
</select>
<template x-for="item in sortedItems" :key="item.id">
<p x-text="`${item.id}: ${item.name}`"></p>
</template>
</div>
过滤 #
html
<div x-data="{
items: [
{ id: 1, name: 'Apple', category: 'fruit' },
{ id: 2, name: 'Carrot', category: 'vegetable' },
{ id: 3, name: 'Banana', category: 'fruit' }
],
filter: 'all',
get filteredItems() {
if (this.filter === 'all') return this.items
return this.items.filter(item => item.category === this.filter)
}
}">
<select x-model="filter">
<option value="all">全部</option>
<option value="fruit">水果</option>
<option value="vegetable">蔬菜</option>
</select>
<template x-for="item in filteredItems" :key="item.id">
<p x-text="item.name"></p>
</template>
</div>
实用示例 #
待办事项列表 #
html
<div x-data="{
todos: [],
newTodo: '',
addTodo() {
if (this.newTodo.trim()) {
this.todos.push({
id: Date.now(),
text: this.newTodo.trim(),
done: false
})
this.newTodo = ''
}
},
removeTodo(id) {
this.todos = this.todos.filter(t => t.id !== id)
},
get remaining() {
return this.todos.filter(t => !t.done).length
}
}">
<h2>待办事项 (<span x-text="remaining"></span> 项未完成)</h2>
<input
x-model="newTodo"
@keyup.enter="addTodo()"
placeholder="添加新任务"
>
<button @click="addTodo()">添加</button>
<ul>
<template x-for="todo in todos" :key="todo.id">
<li :class="{ done: todo.done }">
<input type="checkbox" x-model="todo.done">
<span x-text="todo.text"></span>
<button @click="removeTodo(todo.id)">删除</button>
</li>
</template>
</ul>
</div>
数据表格 #
html
<div x-data="{
users: [
{ id: 1, name: 'John', email: 'john@example.com', role: 'Admin' },
{ id: 2, name: 'Jane', email: 'jane@example.com', role: 'User' },
{ id: 3, name: 'Bob', email: 'bob@example.com', role: 'User' }
],
sortField: 'id',
sortOrder: 'asc',
sortBy(field) {
if (this.sortField === field) {
this.sortOrder = this.sortOrder === 'asc' ? 'desc' : 'asc'
} else {
this.sortField = field
this.sortOrder = 'asc'
}
},
get sortedUsers() {
return [...this.users].sort((a, b) => {
const aVal = a[this.sortField]
const bVal = b[this.sortField]
const order = this.sortOrder === 'asc' ? 1 : -1
return aVal > bVal ? order : -order
})
}
}">
<table>
<thead>
<tr>
<th @click="sortBy('id')">ID</th>
<th @click="sortBy('name')">姓名</th>
<th @click="sortBy('email')">邮箱</th>
<th @click="sortBy('role')">角色</th>
</tr>
</thead>
<tbody>
<template x-for="user in sortedUsers" :key="user.id">
<tr>
<td x-text="user.id"></td>
<td x-text="user.name"></td>
<td x-text="user.email"></td>
<td x-text="user.role"></td>
</tr>
</template>
</tbody>
</table>
</div>
分页列表 #
html
<div x-data="{
items: Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` })),
perPage: 10,
currentPage: 1,
get totalPages() {
return Math.ceil(this.items.length / this.perPage)
},
get paginatedItems() {
const start = (this.currentPage - 1) * this.perPage
return this.items.slice(start, start + this.perPage)
},
goToPage(page) {
this.currentPage = page
}
}">
<ul>
<template x-for="item in paginatedItems" :key="item.id">
<li x-text="item.name"></li>
</template>
</ul>
<div class="pagination">
<button
@click="currentPage--"
:disabled="currentPage === 1"
>
上一页
</button>
<template x-for="page in totalPages">
<button
@click="goToPage(page)"
:class="{ active: currentPage === page }"
x-text="page"
></button>
</template>
<button
@click="currentPage++"
:disabled="currentPage === totalPages"
>
下一页
</button>
</div>
</div>
购物车 #
html
<div x-data="{
cart: [
{ id: 1, name: 'Product A', price: 100, quantity: 2 },
{ id: 2, name: 'Product B', price: 50, quantity: 1 }
],
updateQuantity(id, delta) {
const item = this.cart.find(i => i.id === id)
if (item) {
item.quantity = Math.max(0, item.quantity + delta)
if (item.quantity === 0) {
this.cart = this.cart.filter(i => i.id !== id)
}
}
},
get total() {
return this.cart.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
}">
<table>
<thead>
<tr>
<th>商品</th>
<th>单价</th>
<th>数量</th>
<th>小计</th>
</tr>
</thead>
<tbody>
<template x-for="item in cart" :key="item.id">
<tr>
<td x-text="item.name"></td>
<td x-text="'¥' + item.price"></td>
<td>
<button @click="updateQuantity(item.id, -1)">-</button>
<span x-text="item.quantity"></span>
<button @click="updateQuantity(item.id, 1)">+</button>
</td>
<td x-text="'¥' + (item.price * item.quantity)"></td>
</tr>
</template>
</tbody>
</table>
<p>总计: <strong x-text="'¥' + total"></strong></p>
</div>
注意事项 #
1. 必须使用 template #
html
<div x-for="item in items"></div>
错误!必须使用 <template>:
html
<template x-for="item in items">
<div></div>
</template>
2. 单一根元素 #
<template> 内部应该只有一个根元素:
html
<template x-for="item in items">
<div>元素1</div>
<div>元素2</div>
</template>
推荐使用包装元素:
html
<template x-for="item in items">
<div>
<div>元素1</div>
<div>元素2</div>
</div>
</template>
3. 响应式更新 #
直接通过索引修改数组不会触发更新:
javascript
this.items[0] = newItem
解决方案:
javascript
this.items = [...this.items]
this.items.splice(0, 1, newItem)
4. 性能优化 #
对于大型列表,使用虚拟滚动或分页:
html
<div x-data="{
items: [],
perPage: 20,
currentPage: 1,
get visibleItems() {
const start = (this.currentPage - 1) * this.perPage
return this.items.slice(start, start + this.perPage)
}
}">
<template x-for="item in visibleItems" :key="item.id">
<div x-text="item.name"></div>
</template>
</div>
小结 #
x-for 指令要点:
- 必须在
<template>标签上使用 - 支持数组、对象、数字范围循环
- 使用
:key追踪元素身份 - 支持获取索引
- 支持嵌套循环
下一章,我们将学习 x-model 指令实现双向数据绑定。
最后更新:2026-03-28