customElements.define()是唯一注册入口,标签名须含连字符且全小写(如my-button),禁止原生名;constructor仅调super()、attachShadow等初始化操作;DOM操作和属性读取须在connectedCallback中;attributeChangedCallback不触发初始值,需手动同步;:not(:defined)可防FOUC。

customElements.define() 是唯一能注册自定义元素的入口,不调用它,写再复杂的类也没用——浏览器根本不会识别你的标签。
自定义元素名必须含连字符且小写
像 my-button、date-picker 这种写法合法;mybutton、MyButton、my_button 全部报错:Failed to execute 'define' on 'CustomElementRegistry': The name "xxx" is not a valid custom element name。
原生标签名(div、button、slot、html)也禁止注册,哪怕加了连字符也不行。
constructor 只能初始化,不能读属性或改 DOM
常见错误:在 constructor 里直接写 this.innerHTML = '...' 或 this.getAttribute('label'),结果属性读不到、Shadow DOM 挂不上、甚至抛 NotSupportedError: Failed to execute 'attachShadow'。
正确做法只做三件事:
- 必须先调
super() - 可调
this.attachShadow({ mode: 'open' }) - 可绑定事件或初始化私有字段(如
this._rendered = false)
所有 DOM 操作、模板渲染、属性读取,都得等 connectedCallback 触发后才安全。
attributeChangedCallback 不响应初始值
如果你写了 static get observedAttributes() { return ['label', 'size']; },然后 HTML 里写 <my-button label="确定"></my-button>,attributeChangedCallback 不会自动触发——它只对后续的动态变更(比如 JS 改 el.label = '取消')生效。
所以必须手动同步初始值:
立即学习“前端免费学习笔记(深入)”;
connectedCallback() {
this._render();
}
_render() {
const label = this.getAttribute('label') || '默认';
this.shadowRoot.innerHTML = `<button>${label}</button>`;
}
否则用户看到的就是空按钮或 fallback 内容。
:defined 伪类是防 FOUC 的关键
自定义元素注册有延迟,HTML 解析时若元素尚未定义,浏览器会先当普通 HTMLUnknownElement 渲染,导致样式错乱或内容闪动(FOUC)。
解决办法是在 CSS 中用 :not(:defined) 提前兜底:
my-button:not(:defined) {
visibility: hidden;
}
my-button:defined {
display: inline-block;
opacity: 1;
}
这个细节常被忽略,但上线后首屏白屏或错位,八成就是没加 :defined 控制。



















