customElements.define() 必须在 HTML 解析前同步执行,否则标签被当作 HTMLUnknownElement 处理且永不触发生命周期;attributeChangedCallback 仅响应 observedAttributes 显式声明的属性;connectedCallback 仅首次挂载触发,滚动场景需节点池化复用;disconnectedCallback 不可靠,清理逻辑需双保险。

customElements.define() 必须在 HTML 解析前执行,否则标签永远失效
浏览器遇到 <user-card></user-card> 这类标签时,会**立刻**按 HTMLUnknownElement 处理——它不会等 JS 加载完再判断。一旦错过注册时机,这个节点就再也不会触发 connectedCallback,也不响应属性变更。
常见错误现象:
- 页面写了
<ds-button></ds-button>,但控制台无日志、点击无反应 - Vite 动态导入的组件里调用
customElements.define(),首次访问时组件空白
实操建议:
- 所有
customElements.define()必须收口到一个同步执行的入口文件(如components/index.js),且通过<script defer></script>或模块顶层import加载 - 构建工具需检查:禁止将自定义元素类打包进
import()分支;Webpack 中可加module.rules.exclude = /node_modules/避免误拆 - SSR 场景下必须包裹
if (typeof window !== 'undefined'),否则 Node.js 环境直接报ReferenceError: customElements is not defined
attributeChangedCallback 不生效?先核对 observedAttributes 声明
attributeChangedCallback 不是监听器,它只响应 static get observedAttributes() 显式列出的属性名,且大小写、连字符必须完全一致。
常见错误现象:
- HTML 里写了
<ds-input data-value="abc"></ds-input>,但回调从不执行 - 改了
el.setAttribute('value', 'xyz'),却收到name === 'data-value'的回调 —— 因为实际监听的是'data-value',不是'value'
实操建议:
- 静态 getter 必须返回字符串数组:
static get observedAttributes() { return ['data-value', 'disabled']; } - 属性名严格匹配 HTML 写法:
data-user-id→ 监听'data-user-id',不是'userId'或'user-id' - 首次渲染需手动读取初始值:
this.getAttribute('data-value')放在connectedCallback或constructor(注意:constructor中不能操作 DOM) -
property和attribute同步需手动维护:在 setter 里调用this.setAttribute('data-value', val)
connectedCallback 只触发一次,滚动复用场景得自己管
connectedCallback 仅在元素**首次被 appendChild 到 active DOM 树**时执行。动态切换、innerHTML 替换、塞进 Shadow DOM,都不会再次触发。
常见误判场景:
- 用
innerHTML替换父容器内容 —— 浏览器销毁旧节点、创建新节点,但你逻辑上以为是“复用”,其实是每次都新建 - 动态
document.createElement('my-card')后没调parent.appendChild(el)—— 节点没进 DOM,回调永远不会来 - 把自定义元素塞进 Shadow DOM 内部 —— 它只响应主文档树(
document)的连接,不响应 Shadow 树
实操建议:
- 高频列表/大屏滚动场景,推荐用
Map池化管理节点:初始化设唯一dataset.id,滚出视口时不removeChild,只设el.style.display = 'none'并存入池 - 需要新内容时,优先查池复用:
nodePool.get(id)存在就恢复显示 + 批量更新textContent、dataset - 避免频繁销毁重建:实测 200 节点每秒滚动更新 10 次,layout 时间飙升 3–5 倍
disconnectedCallback 不可靠,清理逻辑必须双保险
disconnectedCallback 是清理定时器、事件监听、AbortController 的主要时机,但浏览器**不保证它一定执行**。页面刷新、标签页后台冻结、进程崩溃时,大概率被跳过。
实操建议:
- 所有异步操作(如
fetch)都配AbortSignal,并在发起前绑定到组件实例;disconnectedCallback中调abort(),但也要在connectedCallback中检查是否已 abort,避免重复调用 - 全局事件监听(如
window.addEventListener('resize', ))必须在disconnectedCallback移除,但也要在connectedCallback中先removeEventListener再addEventListener,防止多次挂载导致监听器堆积 - 不要在
disconnectedCallback里做localStorage写入等关键持久化操作 —— 应在状态变更时实时存,或用beforeunload补充兜底
模板本身不带生命周期,真正起作用的是你对 customElements.define() 时机的控制、对 observedAttributes 的精确声明、以及对复用与清理的主动设计。最容易被忽略的是:节点池化不是可选项,而是滚动场景下的性能底线;disconnectedCallback 不是清理终点,只是其中一环。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











