Web 组件由 Custom Elements 和 Shadow DOM 等标准构成。理解组件的生命周期对创建可维护、性能良好的组件至关重要。本篇文章详细剖析自定义元素的生命周期回调、与标准 DOM 生命周期的关系,以及常见模式和调试技巧。
自定义元素注册与构造
在使用 Web 组件之前,首先用 customElements.define 注册类:
1 | class MyElement extends HTMLElement { |
构造函数在元素被创建时调用,无论是通过 HTML 标签还是 document.createElement。此阶段不建议访问属性或查询子节点,因为它们尚未准备好。
connectedCallback 和 disconnectedCallback
connectedCallback():当元素被插入 DOM 时触发。通常在这里进行绑定事件、初始化数据、发起网络请求等。disconnectedCallback():元素从文档中移除时触发,可用于清理资源或取消订阅。
示例:
1 | connectedCallback() { |
如果一个元素被反复插入和移除,这些回调会多次调用。需谨慎处理避免内存泄漏。
attributeChangedCallback
当观察的属性发生变化时调用。需声明静态的 observedAttributes 数组:
1 | static get observedAttributes() { return ['value', 'disabled']; } |
注意:属性值变化只有在通过 setAttribute/removeAttribute 或属性与属性反射(property reflection)时触发,直接修改 JavaScript 属性不会自动通知。
adoptedCallback
当元素移动到新的文档,例如通过 document.adoptNode 或在 <iframe> 中插入时触发。此在日常开发中较少使用,但大型应用或库中可能遇到。
生命周期与 Shadow DOM
组件可以在生命周期回调中创建 Shadow DOM 模板、插槽内容等:
1 | connectedCallback() { |
若组件可能多次连接/断开,应保护性地检查是否已创建 shadow root。
属性与属性反射
要保持属性和 JS 属性同步,常见模式:
1 | get value() { return this.getAttribute('value'); } |
通过属性反射可以确保 attributeChangedCallback 在 API 使用中正常工作。
观察者与 MutationObserver
有时需要观察子节点变更或属性变更:
1 | this._observer = new MutationObserver(mutations => { /*...*/ }); |
在 disconnectedCallback 中断开观察器以避免泄漏。
生命周期与框架交互
在 React、Vue 等框架中使用 Web 组件时,了解生命周期有助于正确集成。
- React 在
componentDidMount/componentWillUnmount时触发相应 DOM 事件。 - Vue 使用
mounted/beforeDestroy。
需要注意的是,框架的虚拟 DOM 更新可能多次创建/销毁组件实例。
性能优化
- 延迟初始化:在
connectedCallback中仅在必要时加载依赖库。 - 批量操作:在属性大量更改时暂停通知,然后一次更新。
1 | bisectChanges(pairs => { /* 批量处理 */ }); |
- 使用
adoptedCallback处理节点从一个文档移动到另一个文档的情形,以重用对象。
异步与影子 DOM
如果组件需要异步加载资源,可在 connectedCallback 中使用 async / await:
1 | async connectedCallback() { |
注意异步函数引发的错误需要捕获,否则可能破坏微任务队列。
错误处理
在生命周期方法中加入错误边界:
1 | connectedCallback() { |
尤其是在 attributeChangedCallback 中,要处理来自外部的不可信数据。
调试技巧
- 使用
debugger;插入到回调中。 - 在控制台观察
customElements.get('my-element').prototype。 - 通过 Chrome DevTools 的 Event Listener Breakpoints 跳转。
设计模式
- 单例管理:某些组件需要在文档中只存在一个实例,可在构造函数中检查。
- 延迟升级:
customElements.define可以在脚本末尾调用,允许 HTML 先加载并解析标签。
移植性和兼容性
- 低版本浏览器需要 polyfills (
@webcomponents/webcomponentsjs)。 - 使用
whenDefined(name)来等待注册完成。
1 | customElements.whenDefined('my-element').then(() => { /* ... */ }); |
实践示例
1 | class LoadingSpinner extends HTMLElement { |
该组件利用生命周期回调实现简单的开关逻辑。
总结
理解 Web 组件生命周期让你更自如地控制资源、性能和行为。在设计复杂组件时,合理地使用各类回调、观察者以及属性反射模式,可以增强可维护性。牢记:构造阶段只做最必要的设置,连接/断开阶段负责 DOM 交互,属性变更阶段处理外部输入。通过这些原则,你可以构建可靠且长寿命的组件库,为现代前端生态带来更高的可复用性。