Svelte 5 等待异步:experimental.async 实验特性全景 —— 同步更新、fork 与 $effect.pending

发布时间:2026/9/5 22:22:34
Svelte 5 等待异步:experimental.async 实验特性全景 —— 同步更新、fork 与 $effect.pending Svelte 5 等待异步experimental.async 实验特性全景 —— 同步更新、fork 与 $effect.pending【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte从 Svelte 5.36 开始await可以从过去只能在 async 函数内部使用的 JS 语法扩展为可以直接出现在组件的script顶层、$derived(...)声明以及模板表达式中的响应式特性。它配合svelte:boundary的pending片段、$effect.pending()、settled()与 5.42 新增的fork(...)API让 Svelte 应用可以在保持 UI 状态一致性的前提下处理加载态、错误边界与数据预加载。本文基于仓库中的官方文档 19-await-expressions.md 展开并结合编译器与运行时源码讲清这一特性从哪里可用、如何保证一致性、如何显示 loading、出错后去哪、以及如何投机执行。需要说明的前提该特性目前处于实验阶段必须在编译器选项中显式开启experimental.async且实验开关计划在 Svelte 6 中移除届时await将直接可用。开启方式experimental.async 配置该功能必须在配置 Svelte 的地方通常是svelte.config.jsSvelteKit 项目中即 svelte.config.js中开启/// file: svelte.config.js export default { compilerOptions: { experimental: { async: true } } };开启后await在组件中可用位置变为三类组件script的顶层$derived(...)声明内部模板markup表达式内部。从源码看这一门槛在分析阶段就被强制校验。AwaitExpression.js 中当await出现在会挂起的位置顶层或表达式时会检查两个条件// disallow top-level await or await in template expressions // unless a) in runes mode and b) opted into experimental.async if (suspend) { if (!context.state.options.experimental.async) { e.experimental_async(node); } if (!context.state.analysis.runes) { e.legacy_await_invalid(node); } }也就是说未开启experimental.async时会报编译错误且 legacy非 runes模式下同样禁止。对应的运行时开关是一个全局标志flags/index.js 中的async_mode_flag表示编译时设置了experimental.asynctrue后续fork等 API 会依赖它做运行时校验见下文 Forking 一节。同步更新为什么 UI 不会闪出中间态这是整个特性最核心的语义。当一个await表达式依赖某份状态时该状态的变化不会立即反映到 UI 上而是要等异步工作完成后才一并更新从而避免 UI 停留在不一致状态。官方文档给出的例子!-- file: App.svelte -- script let a $state(1); let b $state(2); async function add(a, b) { await new Promise((f) setTimeout(f, 500)); // artificial delay return a b; } /script input typenumber bind:value{a} input typenumber bind:value{b} p{a} {b} {await add(a, b)}/p如果把a从 1 加到 2p不会立刻显示成p2 2 3/p旧结果 新输入的组合而是等add(a, b)解析后整体更新为2 2 4。换句话说表达式中输入侧与结果侧被协调为同一次更新。文档同时指出更新可以重叠—— 一次快速的更新可以在更早一次慢速更新仍在进行时反映到 UI 上。从源码结构看这种全局协调正是基于批处理机制batch.js 中围绕Batch的调度逻辑负责把状态写入、派生重算与 DOM 应用组织进同一批次settled()见下文也返回该批次的完成 promise。并发哪些 await 并行、哪些串行Svelte 会尽可能并行地执行异步工作。例如模板中两个独立的await表达式p{await one(x)}/p p{await two(y)}/p虽然它们在视觉上顺序排列但one与two是相互独立的表达式会同时执行。但这条规则不适用于script顶层或 async 函数中顺序书写的await—— 那些仍然按普通异步 JavaScript 的规则串行执行。有一个重要例外相互独立的$derived表达式会各自独立更新尽管它们首次创建时会按顺序执行。文档给出的例子/** param {number} x */ async function one(x) { return x; } /** param {number} y */ async function two(y) { return y; } let x $state(1); let y $state(2); // b 在 a 解析完成前不会被创建 // 但一旦创建即使 x 和 y 同时变化 // 它们也会独立更新 let a $derived(await one(x)); let b $derived(await two(y));注意写出这种级联等待的代码时预期会收到await_waterfall警告。源码中该警告定义于 warnings.jsexport function await_waterfall(name, location) { if (DEV) { console.warn(%c[svelte] await_waterfall\n%cAn async derived, \${name}\ (${location}) was not read immediately after it resolved. This often indicates an unnecessary waterfall, which can slow down your app\nhttps://svelte.dev/e/await_waterfall, bold, normal); // ... } }即某个异步 derived 在解析后没有被立即读取往往说明存在不必要的瀑布式等待可能拖慢应用。该警告完整说明见 runtime-warnings 参考。另外AwaitExpression.js 中还有pickled_awaits的处理当await前面还有其他表达式、或后面跟着其他表达式时编译器会把await节点记入analysis.pickled_awaits并给表达式打上has_pickled_await标记以便转换阶段恢复正确的反应式上下文——这是模板中{a await b}这类混合表达式的底层支持。指示加载态pending 片段、$effect.pending 与 settled()要渲染占位 UI可以把内容包进带pending片段的svelte:boundary。文档svelte-boundary.md对其行为有明确界定{#snippet pending()} !-- 首次创建时显示后续更新不再显示 -- {/snippet}pending片段只在边界首次创建时显示对于后续异步更新它们是全局协调的改用$effect.pending()。典型场景是在表单字段旁显示正在异步校验你的输入的 spinner。从源码看$effect.pending()由边界块实现boundary.js 中#effect_pending是一个订阅自#local_pending_count的响应式源当计数发生变化时才通过internal_set更新且带effect_pending_outside_reaction的错误约束见 errors.js——即它必须在反应式上下文中使用。如果需要在一段代码里等当前更新彻底完成可以使用settled()它返回一个在当前更新完成时解析的 promise。文档给出的例子import { tick, settled } from svelte; async function onclick() { updating true; // 没有这一步的话updating 的变化会 // 与其他变化归入同一批 // 不会先反映到 UI 上 await tick(); color octarine; answer 42; await settled(); // 此时受 color 或 answer // 影响的更新已全部应用完毕 updating false; }例中color red; answer -1; updating false;为三个状态声明。settled()的运行时实现非常简洁见 runtime.js/** * Returns a promise that resolves once any state changes, * and asynchronous work resulting from them, have resolved * and the DOM has been updated * returns {Promisevoid} * since 5.36 */ export function settled() { return Batch.ensure().settled(); }即settled()就是当前批次含其触发的异步工作完成后 resolve这也解释了为什么先await tick()再改状态、最后await settled()能保证 spinner 的显示与隐藏各自独立成帧。错误处理await表达式中的错误会冒泡到最近的错误边界error boundary即最近的svelte:boundary。这意味着异步逻辑不需要在每个await处手写 try/catch边界组件统一接管失败渲染即可。服务端渲染SSRSvelte 通过render(...)API 支持异步 SSRrender(...)本身返回 promise直接await即可/// file: server.js import { render } from svelte/server; import App from ./App.svelte; const { head, body } await render(App);如果使用 SvelteKit 这类框架这一 await 由框架代劳。SSR 场景下await的具体行为规则见原文档若 SSR 时遇到带pending片段的svelte:boundary则渲染该pending片段边界内其余内容被忽略边界之外或无pending片段的边界内遇到的所有await表达式都会在await render(...)返回之前解析并渲染出内容。原文档还注明未来计划加入流式streaming实现让内容在后台逐步渲染。Forking投机执行与预加载fork(...)API 在 5.42 中加入使得你预期很快会发生的await表达式可以提前运行。它主要面向 SvelteKit 这类框架当用户表现出导航意图如 hover、focus 一个链接时先行预加载数据从而让真正的导航零等待。文档给出的完整示例——在按钮被 focus/hover 时预开菜单指针离开则丢弃点击时提交script import { fork } from svelte; import Menu from ./Menu.svelte; let open $state(false); /** type {import(svelte).Fork | null} */ let pending null; function preload() { pending ?? fork(() { open true; }); } function discard() { pending?.discard(); pending null; } /script button onfocusin{preload} onfocusout{discard} onpointerenter{preload} onpointerleave{discard} onclick{() { pending?.commit(); pending null; // 以防 pending 不存在 // 若存在这句是 no-op open true; }} open menu/button {#if open} !-- 该组件内部的任何异步工作 会在 fork 创建时就开始执行 -- Menu onclose{() open false} / {/if}Fork对象提供commit()异步把投机状态正式应用到 UI与discard()回滚并回收两个操作。源码实现见 batch.js其注释精确描述了语义/** * Creates a fork, in which state changes are evaluated * but not applied to the DOM. * ... * The fn parameter is a synchronous function that * modifies some state. The state changes will be reverted * after the fork is initialised, then reapplied if and * when the fork is eventually committed. * * When it becomes clear that a fork will _not_ be committed * (e.g. because the user navigated elsewhere), it must be * discarded to avoid leaking memory. * since 5.42 */几个值得注意的实现细节入口校验fork未开启async_mode_flag时直接抛experimental_async_required错误——即它和experimental.async是绑定的时机约束fork不能在批次batch执行期间调用否则报fork_timing错误commit 流程把投机期间记录的source.v重新写回并递增 write version主动 flush 受影响的$state.eager效果再batch.flush()并await settled若对已discard的 fork 调用commit会报fork_discarded错误discard 流程为 fork 中变化过的 source 递增 write version让可能变脏的 derived 有机会重算然后丢弃批次。因此使用fork的关键纪律是确定不会提交时务必discard()否则可能泄漏内存源码注释原话。注意事项与破坏性变更注意事项作为实验特性await的处理细节以及$effect.pending()等相关 API有可能在 semver 主版本之外发生破坏性变更官方表示会尽量把这类变更压到最小。Effect 执行顺序的破坏性变更当experimental.async为true时效果的运行顺序会略有不同——{#if ...}、{#each ...}等块级效果现在会先于同一组件中的$effect.pre或beforeUpdate执行。文档指出在极少见情况下在 effect 内部更新状态、且更新了一个本应不再存在的块可能出现异常因此应避免在 effect 中更新状态参见 $effect 文档何时不使用 $effect 一节。小结一张能力对照表需求使用的手段关键行为顶层/derived/模板中使用 awaitexperimental.async: true未开启或非 runes 模式会编译报错避免中间态闪烁依赖驱动的 await 表达式输入与结果同批更新快速更新可覆盖慢速更新并行异步工作模板中独立await表达式并行执行$derived间独立更新首次加载占位 UIsvelte:boundarypending片段仅首次创建时显示SSR 时只渲染 pending后续更新的 loading 指示$effect.pending()需在反应式上下文使用等待更新彻底完成settled()5.36返回当前批次完成 promise基于Batch.ensure()错误兜底最近的svelte:boundary错误向上冒泡至错误边界异步 SSRawait render(App)边界外 await 全部解析后返回预加载/投机执行fork(...)5.42commit()应用、discard()回收勿泄漏所有行为均可在仓库中对应验证编译器侧见 AwaitExpression.js校验与 flags/index.js模式标志运行时侧见 batch.jsfork与批次调度、runtime.jssettled、boundary.js$effect.pending计数与 warnings.jsawait_waterfall。【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考