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