0%

Web组件生命周期

Web 组件由 Custom Elements 和 Shadow DOM 等标准构成。理解组件的生命周期对创建可维护、性能良好的组件至关重要。本篇文章详细剖析自定义元素的生命周期回调、与标准 DOM 生命周期的关系,以及常见模式和调试技巧。

自定义元素注册与构造

在使用 Web 组件之前,首先用 customElements.define 注册类:

1
2
3
4
5
6
7
8
9
class MyElement extends HTMLElement {
constructor() {
super();
// 构造期:这里只能做最基本的初始化
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `<slot></slot>`;
}
}
customElements.define('my-element', MyElement);

构造函数在元素被创建时调用,无论是通过 HTML 标签还是 document.createElement。此阶段不建议访问属性或查询子节点,因为它们尚未准备好。

connectedCallbackdisconnectedCallback

  • connectedCallback():当元素被插入 DOM 时触发。通常在这里进行绑定事件、初始化数据、发起网络请求等。
  • disconnectedCallback():元素从文档中移除时触发,可用于清理资源或取消订阅。

示例:

1
2
3
4
5
6
connectedCallback() {
this.addEventListener('click', this._onClick);
}
disconnectedCallback() {
this.removeEventListener('click', this._onClick);
}

如果一个元素被反复插入和移除,这些回调会多次调用。需谨慎处理避免内存泄漏。

attributeChangedCallback

当观察的属性发生变化时调用。需声明静态的 observedAttributes 数组:

1
2
3
4
5
6
7
8
9
10
11
12
static get observedAttributes() { return ['value', 'disabled']; }

attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case 'value':
this._updateValue(newValue);
break;
case 'disabled':
this._toggleDisabled(newValue !== null);
break;
}
}

注意:属性值变化只有在通过 setAttribute/removeAttribute 或属性与属性反射(property reflection)时触发,直接修改 JavaScript 属性不会自动通知。

adoptedCallback

当元素移动到新的文档,例如通过 document.adoptNode 或在 <iframe> 中插入时触发。此在日常开发中较少使用,但大型应用或库中可能遇到。

生命周期与 Shadow DOM

组件可以在生命周期回调中创建 Shadow DOM 模板、插槽内容等:

1
2
3
4
5
6
7
connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' }).appendChild(
document.getElementById('card-tmpl').content.cloneNode(true)
);
}
}

若组件可能多次连接/断开,应保护性地检查是否已创建 shadow root。

属性与属性反射

要保持属性和 JS 属性同步,常见模式:

1
2
get value() { return this.getAttribute('value'); }
set value(val) { this.setAttribute('value', val); }

通过属性反射可以确保 attributeChangedCallback 在 API 使用中正常工作。

观察者与 MutationObserver

有时需要观察子节点变更或属性变更:

1
2
this._observer = new MutationObserver(mutations => { /*...*/ });
this._observer.observe(this, { childList: true, subtree: true });

disconnectedCallback 中断开观察器以避免泄漏。

生命周期与框架交互

在 React、Vue 等框架中使用 Web 组件时,了解生命周期有助于正确集成。

  • React 在 componentDidMount/componentWillUnmount 时触发相应 DOM 事件。
  • Vue 使用 mounted/beforeDestroy

需要注意的是,框架的虚拟 DOM 更新可能多次创建/销毁组件实例。

性能优化

  • 延迟初始化:在 connectedCallback 中仅在必要时加载依赖库。
  • 批量操作:在属性大量更改时暂停通知,然后一次更新。
1
bisectChanges(pairs => { /* 批量处理 */ });
  • 使用 adoptedCallback 处理节点从一个文档移动到另一个文档的情形,以重用对象。

异步与影子 DOM

如果组件需要异步加载资源,可在 connectedCallback 中使用 async / await

1
2
3
4
async connectedCallback() {
const data = await fetch('/api/info').then(r => r.json());
this._render(data);
}

注意异步函数引发的错误需要捕获,否则可能破坏微任务队列。

错误处理

在生命周期方法中加入错误边界:

1
2
3
4
5
6
7
connectedCallback() {
try {
this._init();
} catch (err) {
console.error('组件初始化失败', err);
}
}

尤其是在 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LoadingSpinner extends HTMLElement {
constructor() {
super();
const sr = this.attachShadow({mode: 'open'});
sr.innerHTML = `<style>.spin{animation:spin 1s linear infinite;}</style>`;
this._spinner = document.createElement('div');
this._spinner.classList.add('spin');
sr.appendChild(this._spinner);
}
connectedCallback() {
if (this.hasAttribute('active')) {
this._spinner.style.display = 'block';
}
}
attributeChangedCallback(name, old, val) {
if (name === 'active') {
this._spinner.style.display = val !== null ? 'block' : 'none';
}
}
static get observedAttributes() { return ['active']; }
}
customElements.define('loading-spinner', LoadingSpinner);

该组件利用生命周期回调实现简单的开关逻辑。

总结

理解 Web 组件生命周期让你更自如地控制资源、性能和行为。在设计复杂组件时,合理地使用各类回调、观察者以及属性反射模式,可以增强可维护性。牢记:构造阶段只做最必要的设置,连接/断开阶段负责 DOM 交互,属性变更阶段处理外部输入。通过这些原则,你可以构建可靠且长寿命的组件库,为现代前端生态带来更高的可复用性。