Svelte `{each}` 块完整指南:列表遍历、键控更新、解构与空态渲染
2026/9/7 10:08:25 网站建设 项目流程

Svelte{#each}块完整指南:列表遍历、键控更新、解构与空态渲染

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

本篇指南基于 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),如MapSet

从实现上可以印证这一点:编译器内部会将这些值统一转换为数组(文档说明内部使用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>

空值语义:如果表达式求值为nullundefined,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。

实践建议小结

  1. 能取稳定 id 就用 key{#each items as item (item.id)}让状态跟随数据条目移动;仅当列表内容绝对静态或纯展示时才省略 key(此时编译器默认按下标做 key);
  2. key 优先选字符串/数字:条目对象整体被替换时身份仍可保持;
  3. 解构要配 key{#each items as { id, name, qty }, i (id)}是"既省事又不丢状态"的推荐组合;
  4. 用无项 each 渲染固定重复内容{#each { length: n }, i}是生成 N 份占位(棋盘、星评、表格行)的惯用写法;
  5. 列表可能为空就写{:else}:编译器会把它编译成空态分支,语义清晰且零成本。

适用前提说明:以上行为描述基于当前仓库的 Svelte 5(runes 时代)实现;legacy 模式(非 runes)下,对列表项的重赋值会触发invalidate式的响应式刷新(客户端转换器中有对应的TODO 6.0注释表明该行为是 legacy 模式专属),具体差异可参考仓库中的 legacy 测试样本 与迁移指南 v5-migration-guide。

【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询