首页
/ Svelte 声明标签({let/const ...})详解:模板内局部变量、作用域规则与编译器实现链路

Svelte 声明标签({let/const ...})详解:模板内局部变量、作用域规则与编译器实现链路

2026-09-04 12:53:23作者:廉彬冶Miranda

声明标签(Declaration Tags)是 Svelte 5 模板语法中定义局部变量的新机制:可以直接在模板任意位置用 {const ...}{let ...} 声明变量,让标记逻辑不再依赖冗长的表达式嵌套。本文基于 Svelte 官方文档 11-declaration-tags.md,结合当前仓库 packages/svelte 中解析、分析、代码生成三阶段的编译器源码,完整讲解声明标签的语法、作用域、响应式用法、限制条件,以及它在 Svelte 编译器内部的实际实现链路。

一、声明标签是什么

声明标签使用 constlet 在标记(markup)内部定义局部变量:

<!--- file: App.svelte --->
<script>
	let boxes = [{ width: 10, height: 10 }, { width: 15, height: 15 }];
</script>

{#each boxes as box}
	{const area = box.width * box.height}
	{const label = `${box.width} × ${box.height} = ${area}`}

	<p>{label}</p>
{/each}

几个关键要点(均来自官方文档原文说明):

  • 版本要求:声明标签自 Svelte 5.56 起可用;
  • {@const} 的关系:传统语法 {@const ...} 已被视为 legacy(遗留语法),官方建议改用声明标签;
  • letconst 的区别const 声明只读常量;let 声明可被重新赋值(文档中 reactive 示例里的 bind:value 就绑定在 let 声明上),这是 {@const} 从未提供过的能力——{@const} 只能声明常量,且只允许作为块({#if}{#each}{#snippet} 等)、组件或 <svelte:boundary> 的直接子节点,而声明标签可以出现在组件内部的任何位置。

仓库中的运行时测试 declaration-tagsdeclaration-tags-eachdeclaration-tags-no-script 分别覆盖了基础用法、{#each} 内使用、以及无 <script> 块等场景,可以按目录下的 _config.js 验证断言。

二、响应式声明标签:let + $state + $derived

当声明的值需要参与响应式系统时,可以在声明标签内部直接使用 $state$derived runes:

<!--- file: App.svelte --->
<script>
	let user = $state({ name: 'Svelte' });
	let editing = $state(false);
</script>

<p>Hello {user.name}</p>
<button onclick={() => editing = true}>edit name</button>

{#if editing}
	{let name = $state(user.name)}
	{const greeting = $derived(`Hello ${name}`)}

	<hr>
	<input bind:value={name} />
	<p>{greeting}</p>

	<button onclick={() => {
		user.name = name;
		editing = false;
	}}>save</button>
{/if}

这个示例体现了声明标签的几个实战价值:

  1. 局部可编辑状态{let name = $state(user.name)} 把父级 state 的一份副本提升为块内局部可写状态,<input bind:value={name} /> 直接绑定它;
  2. 块内派生值{const greeting = $derived(...)} 跟随 name 变化自动重算,无需在 <script> 里为一次性展示逻辑开辟额外变量;
  3. 块级生命周期:由于声明位于 {#if editing} 内,退出编辑状态时该局部状态随块销毁,不会泄漏到组件作用域。

此外,同一个声明标签中也可以包含多个声明符(declarator),且后一个声明符可以引用前一个——这一点在客户端代码生成中有明确注释说明,见 DeclarationTag.js

// register the transformers _before_ visiting the declaration, so that
// later declarators can reference earlier ones
// (e.g. `{let a = $state(0), b = $derived(a * 2)}`)

对应的测试样例位于 declaration-tag-multiple-declarators

三、作用域规则:词法作用域,块级可见性

官方文档明确了作用域语义:声明标签可以使用在组件内部的任何位置;可以引用自身外部声明的值(例如 <script> 标签内或 {#each ...} 块中的值);它对同一词法作用域内的所有代码可见(即兄弟节点,以及兄弟节点的子孙节点)

文档给出的作用域示例完整如下:

<!--- file: App.svelte --->
{const hello = 'hello'}
{hello} <!-- 'hello' -->
<div>
	{const hello = 'hi'}
	{hello} <!-- 'hi' -->
	<div>
		{hello} <!-- 'hi' -->
	</div>
</div>
{hello} <!-- 'hello' -->

可以归纳出三条规则:

  • 顶层 {const hello = 'hello'} 在整个模板顶层词法作用域可见(第一行 {hello} 与结尾 {hello} 都输出 'hello');
  • 嵌套 <div> 内的 {const hello = 'hi'} 遮蔽(shadow) 外层同名变量,且对 <div> 的兄弟与后代都可见(内层两处都输出 'hi');
  • 遮蔽只在词法范围内生效,出了嵌套 <div> 又恢复为 'hello'

在顶层作用域,声明标签不能与 <script> 实例作用域中的变量重名。分析阶段的 DeclarationTag visitor 会检测这种情况并抛出 declaration_duplicate 错误(报错文案见 compile-errors/script.md%name% has already been declared):

const is_top_level = context.path.length === 1 && context.path[0].type === 'Fragment';
if (is_top_level) {
	const duplicate = node.declaration.declarations
		.flatMap((declaration) => extract_identifiers(declaration.id))
		.find((id) => context.state.analysis.instance.scope.declarations.has(id.name));
	if (duplicate) {
		e.declaration_duplicate(duplicate, duplicate.name);
	}
}

四、限制条件与编译错误一览

结合编译器源码与错误消息定义文件,声明标签的约束可以归纳为以下清单:

约束 依据(源码/错误消息位置) 报错信息
只允许 letconst 声明,var/interface/enum 等一律拒绝 parse/state/tag.js 中的 regex_supported_declaration = /(?:let|const)\b/yregex_unsupported_declaration = /(?:var|interface|enum)\b/y declaration_tag_invalid_type:Declaration tags must be let or const declarations(见 compile-errors/template.md
不能用于 legacy 模式(非 runes 组件) 2-analyze/visitors/DeclarationTag.jsif (!runes && !maybe_runes) e.declaration_tag_no_legacy_mode(node) declaration_tag_no_legacy_mode:Declaration tags cannot be used in legacy mode
顶层不能与实例脚本变量重名 见上文分析阶段代码 declaration_duplicate%name% has already been declared
解析失败时的宽松回退(loose 模式) tag.js 的 read_declaration 在 loose 模式下会构造占位 VariableDeclaration 以继续解析 loose-declaration-tag 解析测试

其中值得注意的一个实现细节:解析器对 type 关键字采取"先看形状、再确认解析"的策略。由于 type 不是保留字,它可以合法地出现在表达式中(例如属性访问 x.type),因此 tag.js 中把 type 单独列为"疑似类型声明"的正则(regex_maybe_type_declaration),先尝试用语句解析器完整解析,若结果是 TSTypeAliasDeclaration 才报 declaration_tag_invalid_type 错误,否则回退为普通表达式。

另外,declaration-tag-trailing-slash 这个编译错误样例验证了 {const ...} 后误写斜杠等语法形态会被正确拒绝。

五、编译器实现链路:从解析到代码生成

从源码结构看,声明标签的处理贯穿 Svelte 编译器三个标准阶段(packages/svelte/src/compiler/phases/):

1. 解析阶段(1-parse)

模板遇到 { 后进入 tag statetag() 入口先排除 {#...}{...}{@...}{/...} 等分支,然后调用 read_declaration() 判断是否以 let/const 开头;若是,则用内嵌的语句解析器(parse_statement_at)解析出标准 ESTree VariableDeclaration,最终生成 DeclarationTag AST 节点并挂上空的 ExpressionMetadata。非声明开头则继续按普通 {expression} 处理。

2. 分析阶段(2-analyze)

DeclarationTag visitor 完成三件事:

  • 拒绝 legacy 模式使用(见上节);
  • 检查顶层重复声明(见第三节);
  • in_declaration_tag: true 标记访问声明体,并把 function_depth 对齐到 fragment 作用域——源码注释解释这是为了让 state_referenced_locally 警告("This reference only captures the initial value of %name%")计算正确;
  • 调用 mark_async_declaration():如果声明表达式中包含 awaitmetadata.expression.has_await),或其依赖项中存在 async blocker(闭包读取等),就把该声明纳入 async_consts 分组,生成 promises_id 并为所有绑定设置 blocker,供后续 await 块机制统一处理。

3. 代码生成阶段(3-transform)

客户端与服务器端各有一个 visitor,结构高度对称:

  • 客户端 DeclarationTag:先注册 add_state_transformers(支持 $state/$derived 转换,且保证同一声明内后一声明符可引用前一声明符),再访问声明体;若存在 promises_id 则走 build_async_declaration_parts + add_async_declaration 把声明拆成"先声明 let id,再在 async thunk 里赋值"的形式,并把依赖的 promise 通过 $.wait 等待;否则直接推入 context.state.consts
  • 服务器端 DeclarationTag:逻辑相同,但非异步声明推入 context.state.init(SSR 渲染函数体内的初始化语句),异步路径使用 Promise.all 代替客户端的 $.wait

这与 async-declaration-tagasync-const-closure-read 等运行时测试相互印证:含 await 或读取 await 块内部闭包值的声明标签,会被编译进对应的 async 声明运行(promises 分组),读取方通过 blocker 机制阻塞到值就绪。

六、{@const} 与声明标签的迁移对照

{@const ...} 的官方文档已将其标记为 legacy,并指向声明标签作为替代。对照如下:

维度 {@const x = y} {const x = y} / {let x = y}
可变性 仅常量 const 只读,let 可重新赋值并可 bind:value
允许位置 仅限块、组件、<svelte:boundary> 的直接子节点 组件内任意位置
响应式 依赖所在块的依赖跟踪 可直接使用 $state$derived
状态 legacy,官方建议使用声明标签 Svelte 5.56+ 现行语法

迁移方式通常是一比一的:把块内的 {@const area = box.width * box.height} 替换为 {const area = box.width * box.height} 即可;需要派生语义时可写作 {const x = $derived(y)}(这正是官方文档在 @const 页给出的替代写法)。

七、使用建议

  • 优先用声明标签简化模板:把 {#each} 内反复出现的计算(面积、格式化文案、分支变量)提升为 {const ...},可读性优于内嵌三元与模板串;
  • 需要局部可编辑状态时用 let + $state:编辑弹窗、草稿输入等场景可把块级 $state 声明当作"作用域受限的表单状态";
  • 注意词法遮蔽:内层同名声明会遮蔽外层,调试输出(如示例中 <!-- 'hi' --> 注释)时先确认当前词法层级;
  • runs 模式前提:声明标签要求组件使用 runes 模式(或至少 maybe_runes),在纯 legacy 组件中会得到 declaration_tag_no_legacy_mode 编译错误,跨模式代码库中需要留意。

以上所有行为均可在当前仓库中复核:语法文档见 11-declaration-tags.md,运行时行为见 runtime-runes 声明标签测试样例,编译器错误文案见 compile-errors/template.mdcompile-errors/script.md

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
980
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384