customelements.define() 必须在html解析前同步执行,否则自定义元素将按htmlunknownelement处理,无法触发生命周期钩子;attributechangedcallback仅响应observedattributes声明的属性且严格匹配大小写和连字符;connectedcallback只触发一次,滚动复用需池化;data-*属性需配合虚拟列表等机制才有效。

customElements.define() 必须在 HTML 解析前执行
浏览器遇到 <ds-table></ds-table> 这类标签时,会立刻按 HTMLUnknownElement 处理——不会等 JS 加载完再“补注册”。一旦错过时机,该节点就永远没 connectedCallback、不响应属性变更、也不触发任何生命周期钩子。
常见错误现象:<ds-button></ds-button> 写在 HTML 里,但控制台没日志、点击无反应;Vite 动态路由组件里调用 customElements.define(),首屏空白。
- 所有自定义元素注册必须收口到一个同步执行的入口(如
components/index.js),且通过<script defer></script>或模块顶层import加载 - 构建工具要确保自定义元素类不被拆进
import()分支:Webpack 可加module.rules.exclude = /node_modules/,Vite 需检查build.rollupOptions.external - SSR 场景下必须包裹
if (typeof window !== 'undefined'),否则 Node.js 环境直接报ReferenceError: customElements is not defined
attributeChangedCallback 不生效?先查 observedAttributes
attributeChangedCallback 不是通用监听器,它只响应 static get observedAttributes() 显式声明的属性名,且大小写、连字符必须和 HTML 中完全一致。用 el.size = 'lg' 赋值 property、或写错成 'size'(而实际 HTML 是 data-size),都不会触发。
常见错误现象: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 仅在元素**首次真正挂载到主 DOM 树**时运行。虚拟列表、大屏滚动、Tab 切换等场景中,元素反复进出视口,但 connectedCallback 不会重复触发——你不能靠它做每次显示的初始化。
典型问题:表格行 <ds-row></ds-row> 滚动后内容没更新、状态丢失、事件监听重复绑定。
- 滚动场景必须手动池化复用:预创建固定数量节点(如 30 个),统一设 class,不设初始
data-*,滚动时只更新dataset和textContent - 避免在
connectedCallback里绑定事件监听器;改用事件委托到容器,从e.target.dataset.rowKey拿上下文 - 清理逻辑不能只依赖
disconnectedCallback——它不可靠(如元素被innerHTML = ''清掉时不会触发),需双保险:显式调用销毁方法 +disconnectedCallback作为兜底
data-* 属性不是性能银弹,配合虚拟列表才有效
data-* 属性本身不减少 DOM 节点数、不控制渲染时机,只是数据载体。直接拼接 10 万条带 data-id 的 <div> 塞进页面,只会更卡——DOM 节点数仍是 10 万,layout/paint 压力一分没少。<p>真正起作用的是你怎么用它:配合虚拟列表、节流更新、DOM 复用。</p>
<ul>
<li>在虚拟列表中,<code>data-* 应只绑定当前帧可见项的业务标识,比如 data-user-id 和 data-row-index,值来自 items[startIndex + i]
data-index 存原始数组下标(i),而应存逻辑位置(startIndex + i),防止滚动后索引错乱data-order-id 比 data-id 更易维护;data-row-key 比 data-index 更准确<div data-table="orders"></div>,比每个子节点都写 data-table-name="orders" 更省内存最常被忽略的一点:dataset 是只读映射,无法反向定位真实数组索引——除非你额外维护映射表,而这又引入了同步复杂度和内存开销。











