
Svelte 5 {attach} 标签完全指南Attachment 的用法、组件透传与实现原理【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelteAttachment 是 Svelte 5.29 起提供的模板标签{attach ...}背后的机制它是一个在元素挂载到 DOM 时执行、并在其内部读取的响应式状态更新时自动重跑的函数运行在 effect 语义之中可返回清理函数在重跑前或元素卸载时被调用。本文覆盖 attachment 的基本写法、工厂模式、内联写法、条件挂载、向组件透传、重跑时机控制以及createAttachmentKey、fromAction两个配套 API并结合编译器与运行时的源码实现说明其底层工作原理。什么是 Attachment基本用法按 官方文档 的定义attachment 是“在元素挂载到 DOM 时、或函数内读取的 state 更新时运行在 effect 中的函数”。它可以可选地返回一个函数该函数会在 attachment 重跑之前、或元素后来从 DOM 移除之后被调用。注意Attachments 需要 Svelte 5.29 及以上版本。当前仓库中 packages/svelte/package.json 的版本为 5.57.0已完整包含该能力。最小示例!-- 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 /div其中Attachment的类型定义位于 public.d.ts本质是一个以EventTarget默认为Element为参数的函数可返回一个清理函数export interface AttachmentT extends EventTarget Element { (element: T): void | (() void); }一个元素可以拥有任意多个 attachments它们按各自读取的状态独立重跑。Attachment 工厂返回 attachment 的函数一个非常有用的模式是让某个函数“返回”一个 attachment。以下示例使用tippy.js给按钮加 tooltip并通过bind:value让提示内容跟随输入实时更新!-- 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变化旧的 attachment 会被销毁执行tooltip.destroy、再重新创建。同理attachment 函数首次运行时读取到的任何状态都会触发相同的销毁-重建行为。如果这不是你想要的参考下文“控制 attachment 的重跑时机”。内联 attachmentattachment 也可以直接在模板里内联书写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注意内层的$effect会在color变化时重跑而外层 effect调用canvas.getContext(...)的那层只运行一次因为它没有读取任何响应式状态。条件 attachmentfalse、undefined等假值会被视为“没有 attachment”由此实现条件挂载div {attach enabled myAttachment}.../div当enabled为假时元素不会附加任何东西当它变为真时 attachment 才会执行。对应的行为有专门的测试用例覆盖例如 attachment-component-falsy。向组件传递 attachment当{attach ...}用在组件上时它不会直接作用到组件组件本身没有 DOM 节点而是创建一个以 Symbol 为键的 prop如果该组件随后把 props 展开spread到某个元素上这个元素就会接收到这些 attachments。这就允许你构建能“增强”原生元素的包装组件。例如一个透传所有属性的Button包装组件!-- 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这一机制在源码中可以被精确追踪组件属性转换阶段shared/component.js会处理 attachment 表达式从源码结构看表达式会以expression || $.noop的形式求值这保证了 falsy 值在组件场景下同样安全并作为init形式的 prop 传递给组件。运行时处理 spread 属性的地方attributes.js会遍历对象上的所有 Symbol 属性凡满足symbol.description ATTACHMENT_KEY的属性都会调用attach(element, () n)挂到目标元素上。ATTACHMENT_KEY的值就是字符串attach定义在 constants.js 中。相关行为有 attachment-component、attachment-component-spread 等测试用例验证。控制 attachment 的重跑时机attachment 与 action 不同它是完全响应式的{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()); }); }; }这样昂贵的初始化只运行一次而对getBar()的响应式依赖被隔离到了内层$effect中。以编程方式创建 attachment如果你要给一个稍后会被 spread 到组件或元素上的对象添加 attachment可以使用svelte/attachments导出的createAttachmentKeyscript import { createAttachmentKey } from svelte/attachments; const props { class: cool, onclick: () alert(clicked), [createAttachmentKey()]: (node) { node.textContent attached!; } }; /script button {...props}click me/button从 attachments/index.js 的实现可以看到createAttachmentKey()非常简单——就是返回Symbol(ATTACHMENT_KEY)。正因为运行时靠symbol.description ATTACHMENT_KEY即attach来识别 spread 对象里的 attachment这个 API 对库作者特别有用它提供了一种与{attach ...}等价的、纯编程式的挂载方式构建应用时一般用不到该 API 自 5.29 起可用。把 action 转换为 attachment如果你在使用的第三方库只提供 action 而没有 attachment可以用fromAction把 action 转换成语义相同的 attachment例如这样就可以让 action 用在组件上!-- 使用 action 时 -- div use:foo{bar}.../div !-- 转换为 attachment 后 -- div {attach fromAction(foo, () bar)}.../div这里有一个容易忽略的细节第二个参数如果提供必须是一个“返回” action 参数的函数而不是参数值本身。这正是它能保持响应式语义的关键。查看 fromAction 的实现export function fromAction(action, fn () {}) { return (element) { const { update, destroy } untrack(() action(element, fn()) ?? {}); if (update) { var ran false; render_effect(() { const arg fn(); if (ran) update(arg); }); ran true; } if (destroy) { teardown(destroy); } }; }可以分三点理解它首次调用action(element, fn())时用untrack包裹避免把参数读取追踪到 attachment 自身的外层 effect如果 action 返回了update函数则在render_effect中持续读取fn()每次参数变化时调用update(arg)首个渲染周期跳过因为ran标记如果 action 返回了destroy函数通过teardown注册保证元素移除时清理执行。fromAction自 Svelte 5.32 起可用attachment-from-action 测试用例覆盖了该转换的行为。底层实现{attach}在编译与运行时的完整链路解析阶段解析器在读取元素属性时识别{attach前缀element.js把{attach 表达式}解析为一个AttachTagAST 节点并为其表达式创建ExpressionMetadata用于后续分析。客户端转换阶段客户端 transform 的 AttachTag 访问器 把它编译为$.attach(node, () expression);注意表达式被包在了一个 thunk 里——attachment 表达式的求值被延迟到了运行时 effect 内部。如果表达式是异步的例如涉及await则整个调用会被$.run_after_blockers(...)包裹等待 blocker 解除后再挂载async-attach-blockers、async-attachment 等测试用例覆盖了这一路径。运行时核心attach(node, get_fn)真正的运行时逻辑在 attachments.js全文只有 30 余行export function attach(node, get_fn) { var fn undefined; var e; managed(() { if (fn ! (fn get_fn())) { if (e) { destroy_effect(e); e null; } if (fn) { e branch(() { effect(() fn(node)); }); } } }); }这段代码解释了本文前面多个现象的根源响应式重跑的来源get_fn()即编译期生成的 thunk在managed管理的 effect 中被调用。attachment 工厂表达式如tooltip(content)在这里求值因此其中读取的响应式状态会建立依赖状态变化 → thunk 重算 → 返回的 attachment 函数身份变化。按身份判断是否重建fn ! (fn get_fn())用引用相等判断——只要 attachment 函数换了身份就destroy_effect销毁旧的 effect从而执行 attachment 返回的清理函数再用branch(() effect(fn(node)))创建新的 effect 运行新函数。falsy 即“无 attachment”if (fn)保证假值不会创建任何 effect这正是条件 attachment 和组件场景下|| $.noop兜底能工作的原因。嵌套 effect 支持attachment 函数体本身运行在effect中因此内联 attachment 里的$effect如 canvas 示例可以正常建立自己的依赖与生命周期。源码中还有一个面向未来的 TODO 注释在 6.0/7.0 移除 legacy 模式后block/branch相关逻辑可以进一步简化注释指向 sveltejs/svelte 的 PR 15962。Spread 属性的 attachment 识别当 attachment 通过对象 spread 到达元素时例如组件透传场景attributes.js 在更新属性时遍历Object.getOwnPropertySymbols(next)对每个symbol.description ATTACHMENT_KEY的属性执行effects[symbol] branch(() attach(element, () n))并在属性移除时destroy_effect。这就是“组件 spread props 后元素收到 attachment”的运行时依据。测试用例索引如果你想验证上述任一行为仓库中的运行时测试按场景组织在 packages/svelte/tests/runtime-runes/samples 下与 attachment 直接相关的包括attachment-basic基本挂载与清理attachment-reactive响应式重跑attachment-spread、attachment-spread-stable对象 spread 场景attachment-component、attachment-component-spread、attachment-component-falsy组件透传与 falsy 条件attachment-from-actionaction 转换async-attach-blockers、async-attachment-in-block异步表达式场景svelte-element 支持svelte:element上挂 attachment。解析层与打印层同样有对应样本parser-modern/samples/attachments 与 print/samples/attach-tag。小结{attach ...}提供的是“effect 化”的 DOM 生命周期钩子首次挂载运行、内部状态变化时销毁重建、元素移除时执行清理且天然支持多附件、内联函数与条件挂载。通过工厂函数返回 attachment 是最常用的模式把数据以“返回参数的函数”形式传入并配合内层$effect可以避免昂贵的初始化被响应式重跑拖累。用在组件上时 attachment 变成 Symbol-keyed prop配合{...props}透传可实现包装组件模式createAttachmentKey与fromAction则分别补齐了编程式挂载和旧 action 生态的适配。适用前提Svelte 5.29fromAction需 5.32当前仓库版本 5.57.0 均可使用。【免费下载链接】svelteweb development for the rest of us项目地址: https://gitcode.com/GitHub_Trending/sv/svelte创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考