- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
slotted()是@microsoft/fast-element提供的模板指令(directive),用于观测某个<slot>元素通过assignedNodes()返回的分配节点,并在这些节点发生变化时自动更新组件上的指定属性。本文以仓库 API 文档 为骨架,结合 slotted.ts 源码、node-observation.ts 基类 与 Playwright 测试,完整讲解其函数签名、两种调用方式、可配置选项,以及如何用它在实际组件中构建可响应变化的插槽内容。
一、slotted() 是什么
在 Web Components 的 Shadow DOM 机制中,<slot>是宿主元素与影子树之间的"插槽",外部传入的 Light DOM 内容会被分配到插槽中。当我们需要在组件内部对这些被分配进来的节点(而不是组件自己渲染的节点)做引用、统计或渲染时,slotted()指令就是官方提供的桥梁。
按 fast-element.slotted.md 的官方定义:
A directive that observes the
assignedNodes()of a slot and updates a property whenever they change.
即:观测某个 slot 的assignedNodes()返回值,并在其发生变化时把结果更新到一个属性上。
它属于"引用型指令"(referential directives)家族,与ref(引用单个 DOM 节点)、children(引用某个元素的所有子节点)并列。三者区别如下:
| 指令 | 观测目标 | 底层机制 | 典型场景 |
|---|---|---|---|
ref | 单个模板内节点 | 视图绑定 | 直接拿到video、canvas等节点的引用 |
children | 某元素的所有子节点 | MutationObserver | 统计/遍历某容器下的全部子节点 |
slotted | 某个<slot>的分配节点 | slot 上的slotchange事件 +assignedNodes() | 感知外部传入插槽的内容变化 |
与children使用MutationObserver不同,slotted只监听slotchange事件,成本更低,且天然与 Shadow DOM 的插槽分配语义对齐。
二、函数签名与类型定义
slotted的官方签名(见 fast-element.slotted.md):
export declare function slotted<T = any>( propertyOrOptions: (keyof T & string) | SlottedBehaviorOptions<keyof T & string> ): CaptureType<T>;当前仓库 1.x 系列中,实际导出实现(见 packages/fast-element/src/templating/slotted.ts):
export function slotted<TSource = any, TParent = any>( propertyOrOptions: | (keyof TSource & string) | SlottedDirectiveOptions<keyof TSource & string>, ): CaptureType<TSource, TParent> { if (isString(propertyOrOptions)) { propertyOrOptions = { property: propertyOrOptions }; } return new SlottedDirective( propertyOrOptions as SlottedDirectiveOptions<keyof TSource & string>, ); }对签名做逐项拆解:
- 参数
propertyOrOptions:二选一。- 传一个字符串属性名(
keyof T & string保证该名字必须是源类型T上真实存在的键),函数会自动把它包装为{ property: propertyOrOptions }; - 传一个选项对象(
SlottedBehaviorOptions),可同时配置property、filter以及assignedNodes()的参数;
- 传一个字符串属性名(
- 返回值
CaptureType<TSource, TParent>:一个可供html模板编译器识别的捕获类型标记,表明该指令捕获了模板中的某个节点位置。TSource是视图数据源类型,TParent是父视图类型; - 泛型
<TSource = any, TParent = any>:仓库源码使用TSource/TParent命名,而文档中的 API 摘要使用<T = any>,两者是同一泛型参数在不同生成阶段的不同命名,语义一致。
SlottedBehaviorOptions 选项接口
选项接口定义(fast-element.slottedbehavioroptions.md):
export interface SlottedBehaviorOptions<T = any> extends NodeBehaviorOptions<T>, AssignedNodesOptions它由两部分组成:
NodeBehaviorOptions<T>(定义于 node-observation.ts):property: T:要把观测到的节点数组赋值到的属性名;filter?: ElementsFilter:可选的节点过滤函数,签名(value: Node, index?: number, array?: Node[]) => boolean,对数组中的每个节点调用一次,返回true的节点才会被同步到属性。
AssignedNodesOptions:即浏览器原生HTMLSlotElement.assignedNodes()的参数类型:flatten?: boolean:若为true,返回的节点列表包含所有后备内容(fallback content)被扁平化后的节点;- 这些参数最终原样透传给 getNodes() 中的
assignedNodes(this.options)。
便捷过滤器elements()
仓库在 node-observation.ts 中提供了一个开箱即用的过滤器:
const selectElements = (value: Node): boolean => value.nodeType === 1; export const elements = (selector?: string): ElementsFilter => selector ? value => value.nodeType === 1 && (value as HTMLElement).matches(selector) : selectElements;- 不带参数调用
elements():只保留nodeType === 1的元素节点,过滤掉文本节点、注释节点等; - 带选择器调用
elements('li'):在元素节点的基础上再用matches()做 CSS 选择器匹配; - 该工厂函数同时服务于
children与slotted两个指令,属于共享工具。
三、核心用法:两种调用方式
方式一:直接传属性名
当组件上已经声明了一个属性(配合@observable可获得响应式更新),可以直接把属性名传给slotted():
import { FASTElement, customElement, html, slotted } from '@microsoft/fast-element'; const template = html<MyElement>` <div> <slot ${slotted('slottedNodes')}></slot> </div> `; @customElement({ name: 'my-element', template }) export class MyElement extends FASTElement { @observable slottedNodes: Node[]; slottedNodesChanged() { // 响应插槽节点变化 } }这段示例取自官方 using-directives.md 文档。关键行为:
slottedNodes会被填充为该 slot 的assignedNodes()结果(Node[]);- 属性用
@observable装饰后,插槽内容变化时属性会动态更新,模板中凡是依赖该属性的绑定都会自动重渲染; - 可选的
*Changed变更回调(如slottedNodesChanged())会在节点变化时被调用; - 与
ref、children类似,节点在connectedCallback生命周期之后才可用,官方文档专门给出提示:应优先依赖变更处理器,而不是假设节点在connectedCallback中已就绪。
方式二:传选项对象
当需要过滤节点或指定assignedNodes()参数时,改用对象形式:
import { FASTElement, customElement, html, slotted, elements } from '@microsoft/fast-element'; const template = html<MyElement>` <div> <slot ${slotted({ property: 'items', filter: elements('my-item') })}></slot> </div> `;这里filter: elements('my-item')表示只有匹配my-item选择器的元素节点才会被同步到items属性,文本节点和其他元素都会被过滤掉。
四、运行时行为:源码级实现原理
slotted()返回的SlottedDirective(slotted.ts)继承了NodeObservationDirective基类,四个核心方法构成了完整的观测生命周期:
export class SlottedDirective extends NodeObservationDirective<SlottedDirectiveOptions> { observe(target: EventSource): void { target.addEventListener(slotEvent, this); // slotEvent = "slotchange" } disconnect(target: EventSource): void { target.removeEventListener(slotEvent, this); } getNodes(target: HTMLSlotElement): Node[] { return target.assignedNodes(this.options); } handleEvent(event: Event): void { const target = event.currentTarget as any; this.updateTarget(this.getSource(target), this.computeNodes(target)); } }1. 监听机制:slotchange 事件
observe()在目标<slot>元素上注册slotchange事件监听器,disconnect()负责移除。当插槽的分配节点集合变化(如外部新增/移除子节点、节点被重分配)时,浏览器触发slotchange,指令通过handleEvent触发一次更新。
2. 取值机制:assignedNodes()
getNodes()调用原生assignedNodes(this.options)获取分配节点。传入的flatten等选项会在这里生效。从源码看,slotted不使用MutationObserver,因此只关心"分配到插槽的节点"变化,而非元素子树的任意变动——这是它与children在语义上的根本区别。
3. 过滤与赋值:computeNodes / updateTarget
基类 node-observation.ts 统一处理过滤与赋值:
protected updateTarget(source: any, value: ReadonlyArray<any>): void { source[this.options.property] = value; } protected computeNodes(target: any): Node[] { let nodes = this.getNodes(target); if ("filter" in this.options) { nodes = nodes.filter(this.options.filter!); } return nodes; }即:先取原始节点 → 若有filter则过滤 → 一次性赋值给source[property]。因为属性是@observable的,赋值动作本身就会触发 FAST 的观察者通知机制,驱动依赖该属性的绑定与变更回调。
4. 绑定与解绑生命周期
基类的bind/unbind(node-observation.ts)完成视图绑定时的初始化与清理:
- bind:通过
controller.targets[this.targetNodeId]定位到模板中的<slot>节点,将 controller 挂到节点的内部属性上,立即用当前节点更新一次属性,然后调用observe()注册监听,并登记到controller.onUnbind(this); - unbind:先把属性置为空数组
emptyArray,再disconnect()移除监听,最后清空挂在节点上的 controller 引用——确保视图销毁后不会再有回调泄漏或误更新。
5. 防御性细节
注意SlottedDirective.handleEvent与测试中都体现了两个工程细节:
- 指令把自身作为事件处理器(
this传给addEventListener),并通过event.currentTarget取目标节点,避免了额外的闭包分配; - 测试中专门验证了"DOM 被 JSON.stringify 时不应抛错"(slotted.pw.spec.ts 第 402-441 行),说明指令在节点上挂载的内部 controller 属性被设计为不可序列化侵入的(基类通过
id生成的内部属性名_controllerProperty与noop化等机制规避了序列化问题)。
五、测试用例印证:六种行为契约
仓库的 slotted.pw.spec.ts 用 Playwright 在真实浏览器中验证了指令的行为契约,可作为理解slotted()语义的权威清单:
| 测试 | 验证内容 |
|---|---|
| "returns an SlottedDirective" | slotted("test")返回的确实是SlottedDirective实例 |
| "creates a behavior by returning itself" | 指令的createBehavior()返回自身(无状态无副作用的行为单例) |
| "gathers nodes from a slot" | bind 后,属性被填充为 slot 的全部分配节点,顺序一致 |
| "gathers nodes from a slot with a filter" | 传入filter: elements("foo-bar")后,属性只包含匹配元素 |
| "updates when slotted nodes change" | 向宿主追加子节点并等待Updates.next()后,属性自动同步新增节点 |
| "updates when slotted nodes change with a filter" | 节点变化后,过滤逻辑依然生效 |
| "clears and unwatches when unbound" | unbind 后属性被清空,且后续 DOM 变化不再更新属性 |
| "should not throw if DOM stringified" | 对引用做JSON.stringify不抛异常 |
这组测试从"创建 → 绑定取值 → 过滤 → 动态更新 → 解绑清理 → 序列化安全"六个维度锁定了slotted()的完整行为,读者若在集成时遇到异常,可对照这些断言排查。
六、应用场景与对比选型
典型场景:感知外部传入的插槽内容
slotted()最常见的用途是让组件感知使用者传入的插槽内容。官方文档明确建议:优先使用变更处理器(*Changed)来响应插槽节点变化,而不是假设节点在connectedCallback时已经就绪。例如在fast-foundation系列组件中,select、listbox、toolbar、breadcrumb等组件都大量依赖slotted观测机制来收集插槽中的选项节点并做出响应(相关 API 可参见 fast-foundation 的 slotted 相关 API 文档)。
与children的选型对比
| 维度 | children | slotted |
|---|---|---|
| 观测对象 | 元素的所有子节点(含未插槽的 Light DOM 节点) | 分配到指定<slot>的节点 |
| 底层机制 | MutationObserver(可选subtree) | slotchange事件 +assignedNodes() |
| 选项差异 | 支持subtree(子树观测时必须提供selector) | 支持flatten(扁平化后备内容) |
| 适用场景 | 需要监控容器子树整体变化 | 只关心进入某个插槽的分配结果 |
从 using-directives.md 的提示可知:若把
children放在模板根<template>元素上,拿到的是自定义元素所有 Light DOM 子节点,无论它们是否被插槽分配;而slotted只反映分配到特定<slot>的节点。
七、小结
slotted()是 FAST Element 中连接"外部 Light DOM 内容"与"组件内部逻辑"的关键指令:
- 两种传参:字符串属性名(自动包装)或
SlottedBehaviorOptions对象(可配property、filter、flatten); - 响应式联动:配合
@observable属性与*Changed回调,插槽节点变化时属性自动更新; - 源码机制清晰:
slotchange监听 +assignedNodes()取值 + 可插拔filter,由 SlottedDirective 与 NodeObservationDirective 共同实现; - 行为有测试背书:slotted.pw.spec.ts 覆盖了从绑定、过滤、动态更新到解绑清理的完整契约。
当你需要组件对"外部传入了什么内容、什么时候变化"做出响应时,slotted()就是首选方案——在动手前,请先确认组件的属性已用@observable声明,并优先通过变更处理器响应节点变化。
- 前端
- UI组件
【免费下载链接】fast
The adaptive interface system for modern web experiences.
相关推荐
Fast-Element children() 指令详解:在 FASTElement 中观察与同步子节点
Fast Element children 指令详解:在 FASTElement 中观察与同步子节点 children 是 @microsoft/fast el
前端UI组件深入解析 @microsoft/fast-element 的 ChildrenBehaviorOptions:子节点与子树观察的完整配置指南
深入解析 @microsoft/fast element 的 ChildrenBehaviorOptions:子节点与子树观察的完整配置指南 导读 Childr
前端UI组件FAST Element 的 ChildrenBehavior.disconnect() 详解:子节点观测的拆除与资源回收
FAST Element 的 ChildrenBehavior.disconnect 详解:子节点观测的拆除与资源回收 导读 在基于 @microsoft/fa
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考