Svelte 5 Runes 详解:语言关键字的本质、编译器识别机制与位置校验规则
Runes(符文)是 Svelte 5 引入的一套以 $ 为前缀的语言关键字,用于在 .svelte 和 .svelte.js/.svelte.ts 文件中声明状态、派生值、副作用等响应式逻辑。本文基于 Svelte 官方文档 What are runes? 展开,结合仓库中编译器源码与错误消息定义,讲清 runes 的语义边界、编译器如何识别并校验它们,以及 runes 模式的开启与自动检测机制,帮助你建立对 Svelte 5 响应式编程模型底层规则的系统性理解。
什么是 Runes
[!NOTE] rune /ruːn/ noun:用作神秘或魔法符号的字母或标记。
官方文档给出的定义非常明确:Runes 是在 .svelte 以及 .svelte.js/.svelte.ts 文件中使用的、用于控制 Svelte 编译器的符号。如果把 Svelte 看作一门语言,runes 就是其语法的一部分——它们是"关键字"(keywords)。
从语法形态上看,runes 带有 $ 前缀,外观酷似函数调用:
let message = $state('hello');
但正如文档强调的,它们与普通 JavaScript 函数有几个本质区别,这也是理解 runes 的关键:
- 无需导入——它们是语言的一部分。
$state、$derived等并不存在于任何模块的导出中,编译器在解析阶段直接按语法关键字处理。 - 它们不是值(values)。你不能把 rune 赋值给变量,也不能把它作为参数传给另一个函数。例如
const s = $state;或foo($state(...))之外的"裸引用"都不合法——rune 只在被调用并处于特定位置的表达式里才有意义。 - 与 JavaScript 关键字一样,它们只在特定位置合法。放错位置时编译器会报错——这一点在源码中有非常具体的体现(下文详述)。
[!LEGACY] Runes 在 Svelte 5 之前并不存在。Svelte 5 之前的版本使用
export let、$:反应式语句、stores 自动订阅等"legacy mode"写法。
编译器眼中的完整 Rune 清单
文档中的示例只展示了 $state,但编译器实际维护着一份明确的 rune 注册表。在 utils.js 中,可以看到当前版本支持的全部 rune:
const STATE_CREATION_RUNES = /** @type {const} */ ([
'$state',
'$state.raw',
'$derived',
'$derived.by'
]);
export const RUNES = /** @type {const} */ ([
...STATE_CREATION_RUNES,
'$state.eager',
'$state.snapshot',
'$props',
'$props.id',
'$bindable',
'$effect',
'$effect.pre',
'$effect.tracking',
'$effect.root',
'$effect.pending',
'$inspect',
'$inspect().with',
'$inspect.trace',
'$host'
]);
/**
* @param {string} name
* @returns {name is RuneName}
*/
export function is_rune(name) {
return RUNES.includes(/** @type {RuneName} */ (name));
}
几个值得注意的细节:
- 清单里包含了带点的"组合 rune"(如
$derived.by、$props.id、$inspect().with、$inspect.trace)。这说明编译器识别的不仅是$xxx这种简单标识符,而是"全局调用路径(keypath)"——即MemberExpression链拼出的完整名称。 - 清单被分成
STATE_CREATION_RUNES($state、$state.raw、$derived、$derived.by)与其他 rune 两组,前者通过is_state_creation_rune单独判断,从源码结构看这与"哪些 rune 可以用于声明状态字段"的校验逻辑对应。 - 各 rune 的详细用法散见于 02-runes 目录下的系列文档,如 [state.md?utm_source=gitcode_repo_files)、[derived.md?utm_source=gitcode_repo_files)、[effect.md?utm_source=gitcode_repo_files)、[props.md?utm_source=gitcode_repo_files)、[bindable.md?utm_source=gitcode_repo_files)、[inspect.md?utm_source=gitcode_repo_files)、[host.md?utm_source=gitcode_repo_files),本文聚焦"rune 是什么、编译器如何对待它们"这一总纲。
编译器如何识别一个调用是 Rune
识别入口在 scope.js 的 get_rune 函数:
/**
* Returns the name of the rune if the given expression is a `CallExpression` using a rune.
* @param {Node | null | undefined} node
* @param {Scope} scope
*/
export function get_rune(node, scope) {
if (!node) return null;
if (node.type !== 'CallExpression') return null;
const keypath = get_global_keypath(node.callee, scope);
if (!keypath || !is_rune(keypath)) return null;
return keypath;
}
其工作流程印证了文档中"runes 像函数调用"的说法,但识别逻辑比想象更严格:
- 只识别调用表达式。
CallExpression之外(例如把$state单独当表达式引用)不会被当作 rune 使用——这正是"rune 不是值"在编译器层面的落地。 - 沿着调用者的成员表达式链拼出 keypath。
get_global_keypath会向上回溯非 computed 的MemberExpression链,把属性名逐级拼接(如$state.raw),最终用is_rune(keypath)对照注册表确认是否真的是 rune。 - 要求标识符未被局部作用域遮蔽。keypath 的解析依赖 scope,从源码结构看,如果局部变量遮蔽了对应的全局标识符,就不会被识别为 rune——这与错误样本 invalid-rune-name-shadowed 中"
$xxx被局部声明遮蔽后按普通标识符处理"的行为一致。
位置与参数校验:rune 不是"想放哪就放哪"
文档第三条区别——"只在特定位置合法"——在分析阶段的访问器 CallExpression.js 中有系统性实现。该访问器对每一个被识别出的 rune 做逐一定制化校验,摘选几类典型规则:
位置(placement)校验
$state/$state.raw/$derived/$derived.by:只能作为变量声明的初始化器、类字段声明,或构造函数顶层对实例字段的首次赋值,否则报state_invalid_placement;$props:必须是实例作用域中的变量声明(let props = $props();),且一个组件内只能出现一次,否则报props_invalid_placement或props_duplicate;$bindable:必须出现在$props()的解构模式中作为默认值(let { value = $bindable() } = $props();),否则报bindable_invalid_location;$effect/$effect.pre:必须是独立的语句(ExpressionStatement),不能嵌套在if/for等块内,且必须恰好接收一个参数(回调函数),否则报effect_invalid_placement/rune_invalid_arguments_length;$inspect.trace:必须是所在函数体的第一条语句,且不能位于生成器函数中,否则报inspect_trace_invalid_placement/inspect_trace_generator。
参数(arguments)校验
- 除
$inspect外,任何 rune 调用都不允许展开参数($state(...obj)报rune_invalid_spread); - 各 rune 的参数个数有精确约束,例如
$derived必须恰好一个参数,$props不接受任何参数,违反时报rune_invalid_arguments_length,消息模板为 "%rune%must be called with %args%"。
这些校验产生的错误消息统一定义在 compile-errors/script.md 中,与文档中"编译器会帮助你在位置放错时给出提示"的说法直接对应:
## rune_invalid_arguments
> `%rune%` cannot be called with arguments
## rune_invalid_arguments_length
> `%rune%` must be called with %args%
## rune_invalid_computed_property
> Cannot access a computed property of a rune
## rune_invalid_name
> `%name%` is not a valid rune
## rune_invalid_spread
> `%rune%` cannot be called with a spread argument
## rune_invalid_usage
> Cannot use `%rune%` rune in non-runes mode
## rune_missing_parentheses
> Cannot use rune without parentheses
其中 rune_missing_parentheses("Cannot use rune without parentheses")直接对应了"rune 不是值、必须调用"这条语义:写成 const x = $state; 而非 const x = $state(); 会在此处被拦截。rune_invalid_name 则拦截形如 $foo() 的、不在注册表中的假 rune——测试样本 invalid-rune-name 验证了这一行为。另外值得注意的是 rune_invalid_usage("Cannot use %rune% rune in non-runes mode")的存在,它说明rune 与 runes 模式是绑定的:rune 只能用在 runes 模式的组件里。
Runes 模式:显式开启与自动检测
"rune 在 non-runes 模式下非法"引出一个实际问题:编译器如何知道当前组件处于 runes 模式?仓库源码给出了完整答案,分三层:
第一层:<svelte:options> 标签的显式声明。 解析阶段在 options.js 中处理 runes 属性:
switch (name) {
case 'runes': {
component_options.runes = get_boolean_value(attribute);
break;
}
...
}
即在组件内写 <svelte:options runes />(或 runes={true} / runes={false},get_boolean_value 要求静态布尔值)可以显式锁定模式。runes 同样是合法的编译器 API 参数,见 validate-options.js 中的 runes: parametric(() => undefined) 默认值定义。
第二层:基于组件内容的自动检测。 分析阶段在 2-analyze/index.js 中做如下判定:
const runes =
runes_option ??
(has_await || instance.has_await || Array.from(module.scope.references.keys()).some(is_rune));
含义是:只要没有显式设置 runes 选项,编译器会检测——组件(或其模板)中是否使用了顶层 await,或者 module 作用域是否引用了任何 rune 标识符——满足其一即进入 runes 模式。这解释了为什么 Svelte 5 中写 $state 后无需任何配置 runes 就"自动生效",同时也说明旧式写法($: 标签、export let、store 自动订阅的 $ 前缀引用)在 runes 模式下会被禁止,相关错误定义同样位于 compile-errors/script.md,例如 "$: is not allowed in runes mode, use $derived or $effect instead"、"Cannot use export let in runes mode — use $props() instead"、"Cannot use $$restProps in runes mode" 等。
第三层:$ 前缀语义的复用冲突处理。 在 legacy 模式中 $store 引用表示 store 自动订阅;在 runes 模式中 $ 前缀被 rune 独占。错误消息中专门保留了针对二者的区分("store 订阅的 $ 前缀只能在 .svelte 文件内使用,考虑迁移到 runes"),这也正是文档强调 rune 只存在于 .svelte 与 .svelte.js/.svelte.ts 文件的原因之一。
.svelte.js / .svelte.ts 文件:rune 的第二战场
文档开头即点明 runes 可用在 .svelte 以及 .svelte.js/.svelte.ts 文件中。这一概念由 .svelte.js and .svelte.ts files 专门阐述:
这些文件的行为与任何其他
.js/.ts模块相同,只是你可以使用 runes。这对于创建可复用的响应式逻辑、或在应用内共享响应式状态非常有用(注意:不能导出可被重新赋值的状态)。
结合前文的识别机制可以推断其工作原理:编译器对这两类文件同样运行基于 get_rune/is_rune 的分析管线,因此 get_global_keypath 中"未被局部遮蔽的全局引用"这一前提在纯 JS/TS 模块里同样成立。实践中这意味着:
- 你可以在
.svelte.js中定义$state、$derived、$effect(配合svelte/reactivity提供的运行时能力组织逻辑),把与 UI 无关的响应式逻辑抽离成模块; - 与普通 JS 模块相同的导入导出规则依然适用——rune 本身仍"不是值",不能
export const state = $state;。
小结
回到文档最初的核心命题:runes 是 Svelte 语言层面的关键字,而不是库函数。仓库源码为此提供了完整的证据链:
- utils.js 中的
RUNES注册表与is_rune定义了"哪些调用是 rune"; - scope.js 的
get_rune基于调用表达式 + 全局 keypath + 作用域完成识别,从机制上保证了 rune 不能被赋值、不能被传递(不是值); - 2-analyze/visitors/CallExpression.js 按 rune 逐一校验位置与参数,配套 compile-errors/script.md 中的错误消息,落实"只在特定位置合法";
- 1-parse/read/options.js 与 2-analyze/index.js 共同实现了 runes 模式的显式声明与自动检测。
理解这套机制后,再阅读 [state.md?utm_source=gitcode_repo_files)、[derived.md?utm_source=gitcode_repo_files) 等各 rune 的分册时,你不仅知道"怎么调用",更清楚"为什么只能这样调用"——这正是 runes 作为语言关键字区别于普通 API 的设计本意。
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 StartedRust0624
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