首页
/ Svelte `use:` Action 深度解析:元素挂载副作用的用法、类型体系与编译器/运行时实现

Svelte `use:` Action 深度解析:元素挂载副作用的用法、类型体系与编译器/运行时实现

2026-09-04 20:02:43作者:宗隆裙

本篇基于 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 也不会再次被调用;
  • 如果你需要“参数变化时更新”,文档给出了历史方案与推荐方案:

$effect rune 出现之前,action 可以返回一个带 updatedestroy 方法的对象,其中 update 会在参数变化时以最新值被调用;如今推荐使用 effect 模式。

这个“返回 { update, destroy }”的 legacy 协议在类型系统中仍然保留,后文结合源码说明它为何依然可用、以及为什么新代码应改用 $effect

类型体系:ActionActionReturn

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>;
}

几个值得注意的设计点:

  1. 源码注释(public.d.ts#L68-L69)解释了条件类型用 undefined extends Parameter 而非 Parameter extends undefined 的原因——这样在严格模式与非严格模式下都能正确推断出“参数是否可选”;
  2. Action<HTMLDivElement>Action<HTMLDivElement, undefined> 都表达“该 action 不接受参数”;
  3. 返回值类型 ActionReturnpublic.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();
		}
	});
}

这段实现把文档中的行为约束逐一落实为代码事实:

  1. 只调用一次:action 函数体被放在顶层 effect(...) 内且用 untrack 包裹调用(actions.js#L14-L15)。untrack 使这次调用不订阅任何响应式状态,因此 effect 只在挂载时执行一次,元素卸载时清理函数运行——这正是“挂载时调用、卸载时清理”的语义来源;
  2. legacy update 仍然可用:当 action 返回 update 且存在参数 thunk 时,会额外创建一个 render_effect 跟踪参数。注释解释了为什么要 deep_read_state(value):action 的 update 是粗粒度的,若参数是 $state 对象并发生深度变更,不做深度读取就察觉不到变化;prev 初始化为一个永不相等的哨兵对象,跳过首次调用;
  3. destroy 作为清理:payload 的 destroy 被直接返回为 effect 的清理函数,元素卸载后调用。

对照文档的推荐写法可以看出两种等价路径:文档推荐的 $effect 方案是利用 effect 依赖追踪自动重跑(读到的状态变了就 teardown 后重建),而 legacy update 方案是由运行时显式比较参数变化再手动调用 update。新代码用 $effect,两者在运行时都能工作。

何时改用 {@attach ...}

文档开头即给出建议:

在 Svelte 5.29 及更高版本中,建议使用 attachments,它们更灵活、更可组合。

对照仓库中的 attachments 文档,两者核心差异在于反应式程度:attachment 运行在 effect 中,函数内部读取的状态或工厂表达式依赖变化时会自动销毁重建{@attach foo(bar)} 会随 foobar 的变化重跑),而 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
登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
docsdocs
暂无描述
Markdown
889
5.78 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384
flutter_flutterflutter_flutter
本仓库是 Flutter SDK 与 Flutter Engine 的 OpenHarmony 适配版本,由 CPF-Flutter 团队维护。开发者可使用熟悉的 Flutter 技术栈开发 OpenHarmony 应用,3.35.7 及以后的适配版本可基于本仓库源码构建支持 OpenHarmony 的 Flutter Engine。
Dart
1.17 K
341