Svelte `{each}` 块完整指南:列表遍历、键控更新、解构与空态渲染
本篇指南基于 Svelte 官方文档中 {#each ...} 块的模板语法说明展开,覆盖基本遍历、索引访问、带 key 的键控列表、解构/Rest 模式、无项渲染(重复 N 次)与 {:else} 空态六大核心用法,并结合 Svelte 仓库源码剖析 each 块从编译到运行时协调(diff)的实现链路。读完本文,你既能直接复制可运行的列表写法,也能理解 key 为何能让列表"智能移动"而非整段重建,以及编译器为 each 块生成的底层代码形态。
基本语法与适用的集合类型
each 块的基本形式:
{#each expression as name}...{/each}
带索引的形式:
{#each expression as name, index}...{/each}
被遍历的值可以是以下几类:
- 数组(array);
- 数组类对象(array-like object,即任何带有
length属性的对象); - 可迭代对象(iterable),如
Map和Set。
从实现上可以印证这一点:编译器内部会将这些值统一转换为数组(文档说明内部使用 Array.from 完成转换)。在仓库中,服务端运行时也确实提供了这样的归一化工具 ensure_array_like,服务端编译出的 each 块会调用 $.ensure_array_like(collection) 对表达式求值并归一化(见 服务端 EachBlock 转换器)。
一个典型的购物清单示例:
<h1>Shopping list</h1>
<ul>
{#each items as item}
<li>{item.name} x {item.qty}</li>
{/each}
</ul>
空值语义:如果表达式求值为 null 或 undefined,each 块会按空数组处理——也就是说不会渲染任何列表项,同时会触发 {:else} 分支(如果存在),这与直接传 [] 的行为一致。
带索引遍历
each 块可以指定一个索引变量,语义等价于 array.map(...) 回调的第二个参数:
{#each items as item, i}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/each}
键控 each 块(Keyed each blocks)
语法:
{#each expression as name (key)}...{/each}
{#each expression as name, index (key)}...{/each}
当提供 key 表达式——它必须能唯一标识列表中的每一项——Svelte 会在数据变化时利用 key 智能地更新列表:插入、移动、删除对应的项目,而不是在末尾增删项目、再更新中间的状态。这正是文档强调的核心收益:列表的视觉状态(焦点、输入框内容、组件内部状态等)跟随 key 对应的条目移动,而不是留在原来的 DOM 位置上。
官方建议:key 可以是任意对象,但推荐使用字符串和数字,因为这样即使条目对象本身被替换(对象引用变化),身份(identity)也能保持不变。
{#each items as item (item.id)}
<li>{item.name} x {item.qty}</li>
{/each}
<!-- 或者附带索引 -->
{#each items as item, i (item.id)}
<li>{i + 1}: {item.name} x {item.qty}</li>
{/each}
解构与 Rest 模式
each 块中可自由使用解构(destructuring)和 rest 模式:
{#each items as { id, name, qty }, i (id)}
<li>{i + 1}: {name} x {qty}</li>
{/each}
{#each objects as { id, ...rest }}
<li><span>{id}</span><MyComponent {...rest} /></li>
{/each}
{#each items as [id, ...rest]}
<li><span>{id}</span><MyComponent values={rest} /></li>
{/each}
从客户端编译器的实现看,当上下文是标识符(普通变量)时走一条简单路径;而当上下文是对象/数组解构模式时,客户端 EachBlock 转换器 会通过 extract_paths 展开解构路径,为每个绑定项生成独立的派生信号($.derived / $.derived_safe_equal,带默认值的解构项会走 derived_safe_equal 确保默认值只求值一次),并为每一项注册 read/assign/mutate 行为——这意味着你可以直接在模板里改写解构出来的字段,赋值会写回到原集合对应位置。
无项的 each 块:重复渲染 N 次
如果你只是想把某段内容渲染固定 n 次,可以省略 as 部分:
{#each expression}...{/each}
{#each expression, index}...{/each}
官方文档给出的完整示例——用两层无项 each 渲染一个 8x8 国际象棋棋盘:
<div class="chess-board">
{#each { length: 8 }, rank}
{#each { length: 8 }, file}
<div class:black={(rank + file) % 2 === 1}></div>
{/each}
{/each}
</div>
<style>
.chess-board {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
border: 1px solid black;
aspect-ratio: 1;
.black {
background: black;
}
}
</style>
这里外层 {#each { length: 8 }, rank} 利用了"数组类对象"的能力:一个只有 length 属性的普通对象即可充当可遍历集合,第二个参数 rank 拿到的是 0 到 7 的索引,配合奇偶判断 (rank + file) % 2 === 1 着色黑格。
{:else} 空态分支
{#each expression as name}...{:else}...{/each}
each 块可以带 {:else} 子句:当列表为空(含 null/undefined 的情形)时渲染该分支。
{#each todos as todo}
<p>{todo.text}</p>
{:else}
<p>No tasks today!</p>
{/each}
这一点在服务端编译结果中体现得很直白:服务端 EachBlock 转换器 在有 fallback 时会生成 if (array.length !== 0) { ...for 循环... } else { fallback } 的结构;没有 fallback 时则直接输出一个按 length 遍历的 for 循环:
// 服务端编译产物形态(简化自源码)
const each_array = $.ensure_array_like(expression);
for (let i = 0, $$length = each_array.length; i < $$length; i++) {
let item = each_array[i];
// ...body
}
源码纵深:客户端 each 块的编译与运行时
编译阶段:为每个 each 块计算 flags
客户端 EachBlock 转换器 是理解客户端行为的入口。它会为每个 each 块计算一组位标志(定义在 constants 中):
EACH_INDEX_REACTIVE:键控 each 块带索引时设置。因为条目会移动,索引本身也变成了"响应式"的,读取索引需要经过信号取值;EACH_ITEM_REACTIVE:当列表表达式引用了外部状态(如 store 订阅、跨作用域依赖)时设置,条目需要包在信号里以便追踪;EACH_ITEM_IMMUTABLE:runes 模式下且无 store 时设置;EACH_IS_ANIMATED:当键控 each 的子元素带有animate:指令时设置——注释里说明,由于animate:只能出现在"键控 each 块的唯一子元素"上,所以编译器能在编译期确定该 each 块是否被动画化,从而在协调前后测量动画元素的位置;EACH_IS_CONTROLLED:当该 each 块是父元素的唯一子节点(controlled 形态)时设置,允许运行时走更快的清理路径。
关于 key,编译器会生成一个 key 函数:无 key 时默认使用 index 函数(即直接用下标作为 key);有 key 时则编译为 pattern => keyExpression 形式的箭头函数(见 客户端 EachBlock 转换器 L295-L307)。最终每个 each 块被编译为一次 $.each(flags, thunk, key_function, render_fn, fallback?) 调用。
另外值得注意的细节:如果表达式依赖的是 legacy store 订阅,编译器会在条目变更时生成 $.invalidate_store 调用来反向刷新 store(见 客户端 EachBlock 转换器 L104-L111)。
运行阶段:协调、暂停与快速路径
运行时实现位于 each.js。它的核心机制包括:
- 条目即 effect:每个列表项对应一个独立的 effect(子块),列表更新时按 key 比对,新增项创建 effect、删除项销毁 effect、移动项则重排;
pause_effects批量暂停:删除多个条目时(each.js L66 起),编译器/运行时会先把所有待删 effect 暂停(pause),等待其中若有 out 过渡动画完成后再统一销毁——这就是列表删除时过渡动画能正常播放的底层机制;- controlled 快速路径:当 each 块是父元素的唯一子节点(
EACH_IS_CONTROLLED)、全部条目被删除且没有过渡时,可以直接清空父元素内容并重置锚点,省去逐个销毁的开销; - 压力测试背书:文件头部注释明确要求对该文件的任何实质改动都必须通过 each 块压力测试验证,该测试同时存在于仓库中的 each-stress-test,覆盖了大规模列表增删改移的极端场景。
编译产物的对照验证
如果你想在仓库内验证"文档语法 → 实际生成代码"的对应关系,可以直接查看:
- 服务端转换:server/visitors/EachBlock.js(生成
ensure_array_like+ for 循环 + else 分支); - 客户端转换:client/visitors/EachBlock.js(生成
$.each调用与 key 函数); - 相关行为测试:runtime-runes 测试样本集 中每个含
.svelte与.js断言的目录都是一条可复现的回归用例; - 手动压测:each-stress-test。
实践建议小结
- 能取稳定 id 就用 key:
{#each items as item (item.id)}让状态跟随数据条目移动;仅当列表内容绝对静态或纯展示时才省略 key(此时编译器默认按下标做 key); - key 优先选字符串/数字:条目对象整体被替换时身份仍可保持;
- 解构要配 key:
{#each items as { id, name, qty }, i (id)}是"既省事又不丢状态"的推荐组合; - 用无项 each 渲染固定重复内容:
{#each { length: n }, i}是生成 N 份占位(棋盘、星评、表格行)的惯用写法; - 列表可能为空就写
{:else}:编译器会把它编译成空态分支,语义清晰且零成本。
适用前提说明:以上行为描述基于当前仓库的 Svelte 5(runes 时代)实现;legacy 模式(非 runes)下,对列表项的重赋值会触发 invalidate 式的响应式刷新(客户端转换器中有对应的 TODO 6.0 注释表明该行为是 legacy 模式专属),具体差异可参考仓库中的 legacy 测试样本 与迁移指南 v5-migration-guide。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00