Svelte `use:` Action 深度解析:元素挂载副作用的用法、类型体系与编译器/运行时实现
本篇基于 Svelte 官方文档 use: 指令,完整讲解 use: action 的写法、参数与清理机制、Action 泛型类型体系,并深入当前仓库的编译器与运行时源码,揭示 action 从模板指令到 DOM 副作用的完整执行链路。读完后你将能够:独立编写带参数、带自定义事件、带 TypeScript 完整类型的 action,理解其“只挂载时调用一次”的语义在源码中如何实现,并掌握何时应改用更新的 {@attach ...} 方案。
什么是 action
Action 是元素挂载(mount)时被调用的函数,通过 use: 指令添加。官方文档推荐在 action 内部使用 $effect,这样可以在元素卸载时自动重置/清理状态。文档给出的基础范例:
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/action').Action} */
function myaction(node) {
// the node has been mounted in the DOM
$effect(() => {
// setup goes here
return () => {
// teardown goes here
};
});
}
</script>
<div use:myaction>...</div>
action 也可以接收参数:
<!--- file: App.svelte --->
<script>
/** @type {import('svelte/action').Action} */
function myaction(node, data) {
// ...
}
</script>
<div use:myaction={data}>...</div>
关键调用语义:一次、不在 SSR、不随参数变化重跑
文档明确强调了三条行为约束,这是使用 action 前必须建立的预期:
- action 只会被调用一次(但不会在服务器端渲染阶段执行)——因此它不适合在 SSR 环境中依赖;
- 参数变化不会触发重新调用。action 在挂载时拿到当时的参数值后,即使
data后续变化,myaction也不会再次被调用; - 如果你需要“参数变化时更新”,文档给出了历史方案与推荐方案:
在
$effectrune 出现之前,action 可以返回一个带update和destroy方法的对象,其中update会在参数变化时以最新值被调用;如今推荐使用 effect 模式。
这个“返回 { update, destroy }”的 legacy 协议在类型系统中仍然保留,后文结合源码说明它为何依然可用、以及为什么新代码应改用 $effect。
类型体系:Action 与 ActionReturn
Action 接口接收三个可选泛型参数:节点类型(若 action 适用于一切元素可取 Element)、参数类型、以及 action 创建的自定义事件处理器。定义位于 packages/svelte/src/action/public.d.ts:
export interface Action<
Element = HTMLElement,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
<Node extends Element>(
...args: undefined extends Parameter
? [node: Node, parameter?: Parameter]
: [node: Node, parameter: Parameter]
): void | ActionReturn<Parameter, Attributes>;
}
几个值得注意的设计点:
- 源码注释(public.d.ts#L68-L69)解释了条件类型用
undefined extends Parameter而非Parameter extends undefined的原因——这样在严格模式与非严格模式下都能正确推断出“参数是否可选”; Action<HTMLDivElement>与Action<HTMLDivElement, undefined>都表达“该 action 不接受参数”;- 返回值类型
ActionReturn(public.d.ts#L27-L39)声明了 legacy 协议的两个方法:
export interface ActionReturn<
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
update?: (parameter: Parameter) => void;
destroy?: () => void;
// $$_attributes 仅供类型检查,无运行时作用,应通过 Attributes 泛型设置
}
文档中给出的完整类型范例展示了如何用第三个泛型参数声明自定义事件,从而让 onswipeleft/onswiperight 这类属性获得类型支持:
<!--- file: App.svelte --->
<script>
/**
* @type {import('svelte/action').Action<
* HTMLDivElement,
* undefined,
* {
* onswiperight: (e: CustomEvent) => void;
* onswipeleft: (e: CustomEvent) => void;
* // ...
* }
* >}
*/
function gestures(node) {
$effect(() => {
// ...
node.dispatchEvent(new CustomEvent('swipeleft'));
// ...
node.dispatchEvent(new CustomEvent('swiperight'));
});
}
</script>
<div
use:gestures
onswipeleft={next}
onswiperight={prev}
>...</div>
注意这种模式的前提:Attributes 泛型(以及 $$_attributes)“只影响 TypeScript 类型,运行时没有任何效果”,事件本体仍通过 node.dispatchEvent(new CustomEvent(...)) 在运行时发出,模板侧的 onxxx 属性负责监听。
编译器视角:use: 指令如何被转译
从源码结构看,客户端编译阶段有一个专门的 visitor 处理 use: 指令——UseDirective.js。它生成的初始化语句形如:
$.action($$node, ($$node, $$action_arg) => myaction($$node, $$action_arg), () => data);
对应源码中的关键逻辑:
- 先把 action 函数包成箭头函数(参数为
$$node,如有表达式则再加$$action_arg),再调用maybe_call处理 action 名可能为成员表达式的场景(UseDirective.js#L11-L32); - 有参数时,参数表达式被包成 thunk(
() => data)传入,即运行时拿到的是一个取值函数而非值本身; - 注释明确说明调度顺序:“actions need to run after attribute updates in order with bindings/events”,因此该语句被 push 进块初始化阶段(UseDirective.js#L34-L47);
- 若参数表达式是异步的(如
use:myaction={promise}这类 async 表达式),整段 action 调用会被$.run_after_blockers包裹,推迟到异步阻塞解除之后执行(UseDirective.js#L37-L45)。
这也解释了“action 不在 SSR 时执行”:$.action 是纯客户端 DOM 运行时函数,服务端渲染路径不经过这套元素指令机制。
运行时视角:$.action 的内部实现
运行时入口在 actions.js:
export function action(dom, action, get_value) {
effect(() => {
var payload = untrack(() => action(dom, get_value?.()) || {});
if (get_value && payload?.update) {
// legacy update 协议
var inited = false;
var prev = {}; // initialize with something so it's never equal on first run
render_effect(() => {
var value = get_value();
deep_read_state(value);
if (inited && safe_not_equal(prev, value)) {
prev = value;
payload.update(value);
}
});
inited = true;
}
if (payload?.destroy) {
return () => payload.destroy();
}
});
}
这段实现把文档中的行为约束逐一落实为代码事实:
- 只调用一次:action 函数体被放在顶层
effect(...)内且用untrack包裹调用(actions.js#L14-L15)。untrack使这次调用不订阅任何响应式状态,因此 effect 只在挂载时执行一次,元素卸载时清理函数运行——这正是“挂载时调用、卸载时清理”的语义来源; - legacy
update仍然可用:当 action 返回update且存在参数 thunk 时,会额外创建一个render_effect跟踪参数。注释解释了为什么要deep_read_state(value):action 的update是粗粒度的,若参数是$state对象并发生深度变更,不做深度读取就察觉不到变化;prev初始化为一个永不相等的哨兵对象,跳过首次调用; destroy作为清理:payload 的destroy被直接返回为 effect 的清理函数,元素卸载后调用。
对照文档的推荐写法可以看出两种等价路径:文档推荐的 $effect 方案是利用 effect 依赖追踪自动重跑(读到的状态变了就 teardown 后重建),而 legacy update 方案是由运行时显式比较参数变化再手动调用 update。新代码用 $effect,两者在运行时都能工作。
何时改用 {@attach ...}
文档开头即给出建议:
在 Svelte 5.29 及更高版本中,建议使用 attachments,它们更灵活、更可组合。
对照仓库中的 attachments 文档,两者核心差异在于反应式程度:attachment 运行在 effect 中,函数内部读取的状态或工厂表达式依赖变化时会自动销毁重建({@attach foo(bar)} 会随 foo 或 bar 的变化重跑),而 action 只挂载时执行一次。attachments 还额外支持:
- 内联 attachment(直接在元素上写箭头函数);
- 条件挂载(falsy 值视为无 attachment,如
{@attach enabled && myAttachment}); - 透传给组件(在组件上使用时会生成 Symbol 键的 prop,组件 spread props 时元素即可接收)。
如果手头库只提供 action,attachments 文档也给出了过渡手段:参考 svelte/attachments 模块中的 fromAction 将 action 转换为 attachment(见 documentation/docs/98-reference/21-svelte-attachments.md),从而让基于 action 的库也能用于组件场景。
适用前提与要点小结
- 本文所有行为描述基于当前仓库源码:编译器逻辑见 UseDirective.js,运行时逻辑见 actions.js,类型定义见 public.d.ts;
use:action 仅在客户端挂载时执行,SSR 阶段不会调用,依赖 action 的功能需处理客户端才可见的情况;- action 只执行一次且不因参数变化重跑——需要响应参数变化的逻辑应放在 action 内部的
$effect中,或(Svelte 5.29+)直接迁移到{@attach ...}; - 为 action 提供完整类型时,使用
import('svelte/action').Action<Node, Parameter, Attributes>三个泛型分别约束节点、参数与自定义事件,运行时事件仍需自行dispatchEvent。
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