肉包这几条都落在实处,constructor 不能碰子节点那条我踩过——HTML 解析升级时 light DOM 还没解析完,所以 attachInternals() 和 shadow root 放 constructor 是"建一次"的必需品,操作插槽内容才必须等 connectedCallback。顺着往"第四个组件"再补三点:
1. 属性遮蔽(#upgradeProperty)。 oldV === newV 短路只挡住了循环,挡不住另一种情况:外部在 customElements.define 之前就写了 el.label = 'A',此时没有 setter,值只挂在实例自己的属性上,把原型上的 accessor 遮蔽掉了,升级后 setter 永远收不到。标准做法是在 connectedCallback 里补一次:
#upgrade(name) {
if (this.hasOwnProperty(name)) {
const v = this[name];
delete this[name]; // 删掉实例属性,让原型 accessor 生效
this[name] = v;
}
}
属性/特性混用时,Boolean 型建议用 this.toggleAttribute(name, !!v) 反射,比 setAttribute(name, v) 少一个 "false" 依然是真的坑。
2. 兼容探测要双判。 'adoptedStyleSheets' in Document.prototype 只说明属性在,早期 Safari 有属性但 replaceSync 缺失,稳妥写法是 'replaceSync' in CSSStyleSheet.prototype && 'adoptedStyleSheets' in Document.prototype。另外共享 sheet 是 live 引用——实例里一句 replaceSync 会改掉所有组件,要个性化请 clone 成私有副本或改用 insertRule。
3. composed 再往下挖一层。 即便 composed: true,跨边界后 event.target 会被 retarget 成宿主元素,想拿真实触发者得用 event.composedPath()[0];反过来 composed: false 也不是绝对出不去,shadow root 内部的祖先仍能收到,只是不跨边界。
延伸一句:如果以后要做 SSR/首屏直出,Shadow DOM 默认不输出,得上 Declarative Shadow DOM(<template shadowrootmode="open">),attachShadow 那套就得改成分支写法了。