Svelte `{@const}` 标签详解:局部常量的放置规则、响应式语义与编译器实现
本文围绕 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(块的内容容器),祖父节点必须是 IfBlock、EachBlock、AwaitBlock(对应 {:then}/{:catch})、SnippetBlock、KeyBlock、SvelteFragment、SvelteBoundary、Component/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_depth 和 derived_function_depth 各加一——这正是 $derived 的编译处理方式:表达式会被编译进一个派生信号的回调函数中,依赖变化时惰性重算。
客户端转换:生成 derived 信号,支持解构与异步
在客户端代码生成阶段(3-transform/client),ConstTag 的转换实现 展示了三种情况:
- 普通标识符(如
{@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 };
-
解构模式(如
{@const { width, height } = box}):为整个解构结果创建一个派生信号(临时变量computed_const),派生函数内部按普通变量做解构,再把每个标识符的读取改写为对派生结果取属性($.get(tmp)[name])。注释中也承认这部分逻辑“几乎肯定可以与$derived共享代码”,进一步印证两者同源。 -
异步场景:当表达式中出现
await(node.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-whitespace、const-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 文档。
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 StartedRust0622
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