首页
/ Svelte `{@const}` 标签详解:局部常量的放置规则、响应式语义与编译器实现

Svelte `{@const}` 标签详解:局部常量的放置规则、响应式语义与编译器实现

2026-09-04 14:17:27作者:戚魁泉Nursing

本文围绕 Svelte 模板语法中的 {@const ...} 标签展开:它的作用、严格的放置位置限制、与现代声明标签({const x = $derived(...)})的关系,以及它在 Svelte 编译器分析阶段与客户端/服务端转换阶段的具体实现。读完后,你将能够正确使用这一标签、理解其报错信息的来龙去脉,并从源码层面解释它为何具有响应式语义。

一、{@const} 是什么:在模板中定义局部常量

{@const ...} 标签用于在模板(markup)中定义一个局部常量,最典型的场景是在 {#each} 块内对当前迭代项做一些派生计算,避免在模板里反复书写冗长表达式。官方文档(10-@const.md)给出的标准示例如下:

{#each boxes as box}
	{@const area = box.width * box.height}
	{box.width} * {box.height} = {area}
{/each}

其中 area 只在 {#each} 块内可见,取值随 box 变化而重新计算。

需要特别注意的一点:{@const ...} 已被官方标记为 legacy(旧式)语法。当前文档在开头即给出提示,建议使用声明标签(declaration tags)替代:

{#each boxes as box}
	{const area = box.width * box.height}
	{box.width} * {box.height} = {area}
{/each}

声明标签是 Svelte 5.56 起引入的替代方案,详见 declaration-tags 文档。两者在“块内派生值”这一核心场景下语义一致,但声明标签适用范围更广(不限于块的直接子级,且可直接配合 $state/$derived 表达响应式意图)。如果你的项目已经升级到 5.56+,新代码推荐直接使用 {const ...};理解 {@const} 仍然很有价值,因为它大量存在于存量代码和旧教程中,而且它的编译语义揭示了 Svelte 5 信号化实现的不少细节。

二、放置位置限制:只允许作为“块”的直接子级

{@const} 最容易被写错的地方是放置位置。文档明确规定:

{@const} is only allowed as an immediate child of a block — {#if ...}, {#each ...}, {#snippet ...} and so on — a <Component /> or a <svelte:boundary>.

即:它只能是某个块或组件的直接子节点。编译器错误信息给出了更完整、更精确的白名单,见 编译错误消息定义 中的 const_tag_invalid_placement

{@const} must be the immediate child of {#snippet}, {#if}, {:else if}, {:else}, {#each}, {:then}, {:catch}, <svelte:fragment>, <svelte:boundary> or <Component>

对照这个白名单,以下写法都是不合法的:

  • 放在组件模板的顶层(块外);
  • 放在普通元素内部,如 <div>{@const x = 1}</div>
  • 隔了一层嵌套,不是块的“直接”子节点。

之所以这样限制,是因为 {@const} 的作用域被刻意设计为“块内作用域”——它依赖块(each 的每一项、if 的每一条分支等)提供的上下文变量,因此必须由块来界定其可见范围。

源码印证:分析阶段的位置校验

该限制在编译器分析阶段(2-analyze)中实现,见 ConstTag 分析访问器

const parent = context.path.at(-1);
const grand_parent = context.path.at(-2);

if (
	parent?.type !== 'Fragment' ||
	(grand_parent?.type !== 'IfBlock' &&
		grand_parent?.type !== 'SvelteFragment' &&
		grand_parent?.type !== 'Component' &&
		grand_parent?.type !== 'SvelteComponent' &&
		grand_parent?.type !== 'EachBlock' &&
		grand_parent?.type !== 'AwaitBlock' &&
		grand_parent?.type !== 'SnippetBlock' &&
		grand_parent?.type !== 'SvelteBoundary' &&
		grand_parent?.type !== 'KeyBlock' &&
		((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
			!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {
	e.const_tag_invalid_placement(node);
}

从源码结构看,校验逻辑是检查节点路径:父节点必须是 Fragment(块的内容容器),祖父节点必须是 IfBlockEachBlockAwaitBlock(对应 {:then}/{:catch})、SnippetBlockKeyBlockSvelteFragmentSvelteBoundaryComponent/SvelteComponent,或者是一个带有 slot 属性的普通元素/<svelte:element>(即通过 slot 传递进组件的片段,此时作用域归属于宿主组件的插槽内容)。这一实现与错误消息文案中的白名单一一对应,多出来的 KeyBlock 与 slot 分支正是文档中 “and so on” 所涵盖的内容。

此外,同一文件中还有 const_tag_cycle(循环依赖,见后文)与 const_tag_invalid_expression(“must consist of a single variable declaration”,即只允许单个变量声明)两类校验,分别对应诸如 a = b; b = a 式的互相引用、以及把 {@const} 的表达式写成非声明语句等错误。

三、响应式语义:{@const} 在内部被当作 $derived 处理

{@const} 定义的常量不是“一次求值、永不再变”的普通 const,而是随其依赖的响应式数据自动更新的派生值。这一语义在分析阶段的源码注释中写得很直白,见 ConstTag 分析访问器

context.visit(declaration.init, {
	...context.state,
	expression: node.metadata.expression,
	// We're treating this like a $derived under the hood
	function_depth: context.state.function_depth + 1,
	derived_function_depth: context.state.function_depth + 1
});

也就是说,编译器在遍历初始化表达式时,把 function_depthderived_function_depth 各加一——这正是 $derived 的编译处理方式:表达式会被编译进一个派生信号的回调函数中,依赖变化时惰性重算。

客户端转换:生成 derived 信号,支持解构与异步

在客户端代码生成阶段(3-transform/client),ConstTag 的转换实现 展示了三种情况:

  1. 普通标识符(如 {@const area = box.width * box.height}):直接调用 create_derived 生成一个派生信号,注册进当前作用域的 transform 表,后续引用 area 时编译为对该信号的读取(get_value)。开发模式下还会额外包一层 $.tag 以便 DevTools 中标识这是一个 const 标签:
let expression = create_derived(context.state, init, node.metadata.expression);
if (dev) {
	expression = b.call('$.tag', expression, b.literal(declaration.id.name));
}
context.state.transform[declaration.id.name] = { read: get_value };
  1. 解构模式(如 {@const { width, height } = box}):为整个解构结果创建一个派生信号(临时变量 computed_const),派生函数内部按普通变量做解构,再把每个标识符的读取改写为对派生结果取属性($.get(tmp)[name])。注释中也承认这部分逻辑“几乎肯定可以与 $derived 共享代码”,进一步印证两者同源。

  2. 异步场景:当表达式中出现 awaitnode.metadata.promises_id 存在,如 {#await} 块内)时,不再使用 const 声明,而是走 add_async_declaration 生成 let 并在 await 链完成后赋值,见 客户端转换的 add_const_declaration。开发模式下还会主动 $.get(id) 一次,以提前触发 “Cannot access x before initialization” 这类错误。

服务端转换:无信号,普通 const

服务端(SSR)渲染不存在持续更新的场景,因此 服务端转换 简单得多:直接生成 const id = init 推入初始化语句序列;仅当表达式含 await 时同样走异步声明路径。这与客户端的“信号化”处理形成对照:同一个 {@const} 标签,客户端编译为响应式派生,服务端编译为一次性求值的局部变量。

循环依赖的静态检测

{@const} 之间可以互相引用(只要不构成环),例如 {@const b = a + 1} 引用前一个 {@const a = 2} 是合法的。但一旦出现环,分析阶段会报 const_tag_cycle 错误。仓库中的编译错误测试用例 const-tag-cyclical 验证了这一点,期望错误为 Cyclical dependency detected: a → b → a。另有 const-tag-whitespaceconst-tag-sequence 等用例覆盖空白与序列表达式等边角情况,目录均位于 tests/compiler-errors/samples 下,可对照 错误消息定义 查看完整报错文案。

四、与现代声明标签的关系与迁移建议

{@const}{const ...} 的差异可以从三个维度总结:

维度 {@const x = y}(legacy) {const x = y}(声明标签,5.56+)
放置位置 仅块的直接子级({#if}{#each}、组件、<svelte:boundary> 等) 组件内任意位置,遵循词法作用域
作用域 所在块内 同一词法作用域内对兄弟及后代可见
响应式表达 隐式派生(内部按 $derived 处理) 显式结合 $derived/$state,意图更清晰

官方文档的态度很明确:declaration-tags 文档 中注明 “The {@const ...} syntax is considered legacy — use declaration tags instead”。声明标签还支持在块内声明可变局部状态,这是 {@const} 做不到的:

{#if editing}
	{let name = $state(user.name)}
	{const greeting = $derived(`Hello ${name}`)}
	<input bind:value={name} />
	<p>{greeting}</p>
{/if}

因此实际项目中的建议是:维护旧代码时理解 {@const} 的上述限制与语义即可;新代码(Svelte ≥ 5.56)统一改用 {const ... = $derived(...)}{let ... = $state(...)}。Svelte 也提供了编译器迁移工具(见 migrate 模块)辅助批量转换旧语法,仓库中 tests/migrate/samples 收录了大量迁移前后对比用例可供参考。

五、小结

  • {@const ...} 用于在模板块内定义局部常量,典型用法是在 {#each} 中计算派生值(如面积 box.width * box.height)。
  • 只能是块({#if}/{:else}{#each}{:then}/{:catch}{#snippet}<svelte:fragment>)或 <Component>/<svelte:boundary> 的直接子级,位置违规会触发 const_tag_invalid_placement 编译错误,校验逻辑见 2-analyze 的 ConstTag 访问器
  • 其语义等价于“块内 $derived”:客户端编译为派生信号(支持解构与 await),服务端编译为普通 const;循环引用会被静态检测并报 const_tag_cycle
  • 该语法已标记为 legacy,Svelte 5.56+ 请改用声明标签 {const x = $derived(y)},参考 declaration-tags 文档
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
903
1.82 K
docsdocs
暂无描述
Markdown
888
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.51 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341