多实例组件必须通过 Custom Elements 的 constructor 初始化私有状态,而非共享闭包或全局变量;Shadow DOM 仅隔离样式与节点,状态隔离需依赖 this 上下文自治,且副作用须绑定并清理于实例自身。

直接用 <template> 做多实例组件,不加状态管理机制,必然出现实例间互相干扰——比如一个 my-counter 的点击计数被另一个实例重置,或共享了同一个全局变量。
为什么 cloneNode(true) 不能直接用于多实例状态隔离
模板克隆只是复制 DOM 结构,不复制 JS 执行上下文。所有实例共用同一份初始化逻辑,若在工厂函数里用了闭包变量或全局对象(如 let count = 0),所有实例就会读写同一个值。
- 用
document.getElementById('tpl').content.cloneNode(true)只解决结构复用,不解决状态归属 - 若模板内含
<input id="name">,多个实例会因重复 ID 导致document.getElementById('name')返回不可预测的元素 - 事件监听器若绑定在 document 或父容器上,容易监听到其他实例的事件,没做 target 过滤就出错
用 Custom Elements + constructor 实例化私有状态
每个自定义元素实例天然拥有独立的 this 上下文,是承载状态最自然的容器。关键不是“把 HTML 塞进去”,而是让每个实例自己 hold 住自己的数据。
- 在
constructor()中调用super()后立即初始化状态字段:this.count = 0、this.isOpen = false - 不要在
connectedCallback()里初始化状态,它可能被反复触发(比如元素被移除又 append 回来) - 属性变更响应必须走
attributeChangedCallback(),且只更新对应字段,避免覆盖整个状态对象 - 若需从 HTML 属性同步初始值(如
<my-accordion open>),在constructor()里用this.hasAttribute('open')判断,别等attributeChangedCallback—— 它对初始属性不触发
Shadow DOM 内部如何安全访问和更新状态
Shadow DOM 不是状态隔离的银弹,它只隔离样式和节点,状态仍要靠 JS 控制。但它的作用是防止外部脚本意外修改内部 DOM,从而保护状态与视图的一致性。
立即学习“前端免费学习笔记(深入)”;
- 所有 DOM 查询必须通过
this.shadowRoot.querySelector(),不能用document.querySelector() - 更新视图时,优先操作具体节点属性(如
el.textContent = this.count),而不是重写this.shadowRoot.innerHTML—— 后者会销毁已绑定的事件监听器和 slot 内容 - 如果模板里用了
<slot>,确保状态变更后仍保留用户插入的内容;不要在渲染逻辑中忽略<slot>节点 - 避免在
attributeChangedCallback()中直接调用this.render()全量重绘,应只更新受影响的子节点,否则 slot 里的动态内容(如 Vue 子组件)会丢失
真正难的不是让五个 my-toggle 都能切换,而是当其中一个 toggle 触发 API 请求失败时,它自己的 loading 状态不波及其他四个——这要求每个实例的状态完全自治,且所有副作用(事件、请求、定时器)都绑定在 this 上,销毁时能被正确清理。



















