首页
/ Svelte 5 Runes 详解:语言关键字的本质、编译器识别机制与位置校验规则

Svelte 5 Runes 详解:语言关键字的本质、编译器识别机制与位置校验规则

2026-09-06 22:45:09作者:何将鹤

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 的关键:

  1. 无需导入——它们是语言的一部分$state$derived 等并不存在于任何模块的导出中,编译器在解析阶段直接按语法关键字处理。
  2. 它们不是值(values)。你不能把 rune 赋值给变量,也不能把它作为参数传给另一个函数。例如 const s = $state;foo($state(...)) 之外的"裸引用"都不合法——rune 只在被调用并处于特定位置的表达式里才有意义。
  3. 与 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](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/02state](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/02-state.md?utm_source=gitcode_repo_files)、[derived](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/03derived](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/03-derived.md?utm_source=gitcode_repo_files)、[effect](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/04effect](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/04-effect.md?utm_source=gitcode_repo_files)、[props](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/05props](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/05-props.md?utm_source=gitcode_repo_files)、[bindable](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/06bindable](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/06-bindable.md?utm_source=gitcode_repo_files)、[inspect](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/07inspect](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/07-inspect.md?utm_source=gitcode_repo_files)、[host](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/08host](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/08-host.md?utm_source=gitcode_repo_files),本文聚焦"rune 是什么、编译器如何对待它们"这一总纲。

编译器如何识别一个调用是 Rune

识别入口在 scope.jsget_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 像函数调用"的说法,但识别逻辑比想象更严格:

  1. 只识别调用表达式CallExpression 之外(例如把 $state 单独当表达式引用)不会被当作 rune 使用——这正是"rune 不是值"在编译器层面的落地。
  2. 沿着调用者的成员表达式链拼出 keypathget_global_keypath 会向上回溯非 computed 的 MemberExpression 链,把属性名逐级拼接(如 $state.raw),最终用 is_rune(keypath) 对照注册表确认是否真的是 rune。
  3. 要求标识符未被局部作用域遮蔽。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_placementprops_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 语言层面的关键字,而不是库函数。仓库源码为此提供了完整的证据链:

理解这套机制后,再阅读 [state](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/02state](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/02-state.md?utm_source=gitcode_repo_files)、[derived](https://gitcode.com/GitHubTrending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02runes/03derived](https://gitcode.com/GitHub_Trending/sv/svelte/blob/b2c22ab66d0978dfa14bb92196a6ab45ad0031a4/documentation/docs/02-runes/03-derived.md?utm_source=gitcode_repo_files) 等各 rune 的分册时,你不仅知道"怎么调用",更清楚"为什么只能这样调用"——这正是 runes 作为语言关键字区别于普通 API 的设计本意。

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