{@attach ...}

附件(attachments)是一些函数,当元素被挂载到 DOM 时,或当函数内部读取的状态 发生更新时,它们会在一个effect 中运行。

可选地,它们可以返回一个函数,该函数在附件重新运行之前,或元素之后从 DOM 中移除时被调用。

[!NOTE] 附件在 Svelte 5.29 及更高版本中可用。

<!--- file: App.svelte --->
<script>
	/** @type {import('svelte/attachments').Attachment} */
	function myAttachment(element) {
		console.log(element.nodeName); // 'DIV'

		return () => {
			console.log('cleaning up');
		};
	}
</script>

<div {@attach myAttachment}>...</div>

一个元素可以拥有任意数量的附件。

附件工厂

一种有用的模式是让某个函数(例如下面示例中的 tooltip)_返回_一个附件(演示):

<!--- file: App.svelte --->
<script>
	import tippy from 'tippy.js';

	let content = $state('Hello!');

	/**
	 * @param {string} content
	 * @returns {import('svelte/attachments').Attachment}
	 */
	function tooltip(content) {
		return (element) => {
			const tooltip = tippy(element, { content });
			return tooltip.destroy;
		};
	}
</script>

<input bind:value={content} />

<button {@attach tooltip(content)}>
	Hover me
</button>

由于 tooltip(content) 表达式是在一个effect 内部运行的,因此每当 content 发生变化时,该附件都会被销毁并重新创建。对于在附件函数首次运行时_内部_读取的任何状态,也会发生同样的情况。(如果这不是你想要的,请参阅控制附件何时重新运行。)

内联附件

附件也可以以内联方式创建(演示):

<!--- file: App.svelte --->
<canvas
	width={32}
	height={32}
	{@attach (canvas) => {
		const context = canvas.getContext('2d');

		$effect(() => {
			context.fillStyle = color;
			context.fillRect(0, 0, canvas.width, canvas.height);
		});
	}}
></canvas>

[!NOTE] 嵌套的 effect 会在 color 发生变化时运行,而外部的 effect(即调用 canvas.getContext(...) 的地方)只运行一次,因为它没有读取任何响应式状态。

条件式附件

falseundefined 这样的假值会被视为"无附件",从而支持条件式使用:

<div {@attach enabled && myAttachment}>...</div>

将附件传递给组件

当用于组件时,{@attach ...} 会创建一个键为 Symbol 的 prop。如果该组件随后将 props 展开 到一个元素上,该元素就会接收到这些附件。

这让你能够创建_包装组件_,用以增强元素(演示):

<!--- file: Button.svelte --->
<script>
	/** @type {import('svelte/elements').HTMLButtonAttributes} */
	let { children, ...props } = $props();
</script>

<!-- `props` includes attachments -->
<button {...props}>
	{@render children?.()}
</button>
<!--- file: App.svelte --->
<script>
	import tippy from 'tippy.js';
	import Button from './Button.svelte';

	let content = $state('Hello!');

	/**
	 * @param {string} content
	 * @returns {import('svelte/attachments').Attachment}
	 */
	function tooltip(content) {
		return (element) => {
			const tooltip = tippy(element, { content });
			return tooltip.destroy;
		};
	}
</script>

<input bind:value={content} />

<Button {@attach tooltip(content)}>
	Hover me
</Button>

控制附件何时重新运行

actions 不同,附件是完全响应式的:{@attach foo(bar)} 会在 foo bar(或 foo 内部读取的任何状态)发生变化时重新运行:

function foo(bar) {
	return (node) => {
		veryExpensiveSetupWork(node);
		update(node, bar);
	};
}

在极少数情况下,这会成为问题(例如,如果 foo 做了昂贵且不可避免的设置工作),可以考虑将数据放在一个函数内部传递,并在一个子 effect 中读取它:

function foo(getBar) {
	return (node) => {
		veryExpensiveSetupWork(node);

		$effect(() => {
			update(node, getBar());
		});
	}
}

以编程方式创建附件

要向一个将被展开到组件或元素上的对象添加附件,请使用 createAttachmentKey

将 actions 转换为附件

如果你正在使用一个只提供 actions 的库,可以通过 fromAction 将它们转换为附件,从而让你可以(例如)将它们与组件一起使用。