news 2026/9/5 22:22:33

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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.js,SvelteKit 项目中即 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.async=true",后续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 type="number" bind:value={a}> <input type="number" bind:value={b}> <p>{a} + {b} = {await add(a, b)}</p>

如果把a从 1 加到 2,<p>不会立刻显示成<p>2 + 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>

虽然它们在视觉上顺序排列,但onetwo是相互独立的表达式,会同时执行

但这条规则不适用于<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.js:

export 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 {Promise<void>} * @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,边界组件统一接管失败渲染即可。

服务端渲染(SSR)

Svelte 通过render(...)API 支持异步 SSR:render(...)本身返回 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.asynctrue时,效果的运行顺序会略有不同——{#if ...}{#each ...}块级效果现在会先于同一组件中的$effect.prebeforeUpdate执行。文档指出,在极少见情况下(在 effect 内部更新状态、且更新了一个本应不再存在的块)可能出现异常,因此应避免在 effect 中更新状态(参见 $effect 文档"何时不使用 $effect" 一节)。

小结:一张能力对照表

需求使用的手段关键行为
顶层/derived/模板中使用 awaitexperimental.async: true未开启或非 runes 模式会编译报错
避免中间态闪烁依赖驱动的 await 表达式输入与结果同批更新,快速更新可覆盖慢速更新
并行异步工作模板中独立await表达式并行执行;$derived间独立更新
首次加载占位 UI<svelte:boundary>+pending片段仅首次创建时显示;SSR 时只渲染 pending
后续更新的 loading 指示$effect.pending()需在反应式上下文使用
等待更新彻底完成settled()(5.36)返回当前批次完成 promise,基于Batch.ensure()
错误兜底最近的<svelte:boundary>错误向上冒泡至错误边界
异步 SSRawait render(App)边界外 await 全部解析后返回
预加载/投机执行fork(...)(5.42)commit()应用、discard()回收,勿泄漏

所有行为均可在仓库中对应验证:编译器侧见 AwaitExpression.js(校验)与 flags/index.js(模式标志);运行时侧见 batch.js(fork与批次调度)、runtime.js(settled)、boundary.js($effect.pending计数)与 warnings.js(await_waterfall)。

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 22:14:31

Cesium填挖方分析组件:空间对齐与混合算法工程实践

简介&#xff1a;本资源是一个面向GIS前端开发者的Cesium三维地理信息分析功能组件&#xff0c;聚焦于工程土方量计算中的填挖方分析场景&#xff0c;特别适配需同时处理真实地形与BIM/倾斜摄影模型的项目需求。资源包共4个文件&#xff0c;含2个核心JavaScript模块&#xff08…

作者头像 李华
网站建设 2026/9/5 22:12:45

基于Netty与Java构建高并发MUD游戏服务器:架构设计与工程实践

简介&#xff1a;本资源是吉林大学Java程序设计课程高分课程设计成果——MUD多人在线文字冒险游戏系统源码&#xff0c;面向Java初学者及课程设计、期末大作业、毕业设计实践者&#xff0c;解决网络编程、多线程会话管理与实时交互等核心教学难点。压缩包共40个文件&#xff0c…

作者头像 李华
网站建设 2026/9/5 22:11:31

调和映射与奇异空间:从正则性理论到液晶缺陷建模

1. 问题背景&#xff1a;为什么 CSDN 上很难搜到这篇报告 先给结论&#xff1a;这次大会报告涉及的是调和映射理论中的 奇异空间 分支&#xff0c;属于几何分析、PDE 与度量几何的交叉方向。 很多做工程和算法的读者看到“调和映射”会先想到图像修复、网格参数化、弹性形变…

作者头像 李华
网站建设 2026/9/5 22:07:30

多AI-Agent协作的量化工作台:盘前盘中盘后自动化闭环架构

前阵子有个读者给我留言&#xff0c;问我一个人怎么盯盘前、盘中、盘后三套流程&#xff0c;还要兼顾策略优化和风控&#xff0c;是不是得组个团队才能扛下来。我当时正好在做QuantBot——一个多AI-Agent协作的量化工作台&#xff0c;把从盘前数据准备、信号生成&#xff0c;到…

作者头像 李华
网站建设 2026/9/5 22:07:16

Telegram X多平台构建指南:从Android到Huawei AppGallery的完整流程

Telegram X多平台构建指南&#xff1a;从Android到Huawei AppGallery的完整流程 &#x1f680; Telegram X作为Telegram官方推出的实验性Android客户端&#xff0c;凭借其流畅的界面体验和强大的多平台支持&#xff0c;已成为众多用户的首选。本指南将带你深入了解Telegram X的…

作者头像 李华