跳到主要内容

Web 组件概览与自定义元素

Web 组件由哪几项浏览器规范组成?​

  • 出发点: 把 UI 拆成自治的可视组件, 每个组件有自己的位置、职责与交互边界;
  • 设计原则: 不要写出复杂软件, 复杂就拆成简单部分再以最明显的方式连接;
  • 组件要素: 自己的 JS 类; 只由该类管理的 DOM 结构 (外部代码不访问); 作用于组件的 CSS; 对外 API (事件、方法等);
  • Custom elements: 定义自定义 HTML 标签;
  • Shadow DOM: 组件内部 DOM, 对外隐藏;
  • CSS Scoping: 只作用于组件影子树的样式规则;
  • Event retargeting: 让外部看到的事件像是发生在宿主元素上, 见 Shadow DOM 事件与组件模式;
  • 现状: 自定义元素支持最好、可独立使用; 其余规范仍在演进, Chrome 跟进最快;

自定义元素的名字有什么硬性规则?​

  • 必须含连字符: my-element、super-button 合法, myelement 非法;
  • 原因: 保证自定义标签与内置 HTML 标签永不重名;
  • autonomous: 继承 HTMLElement 的全新标签, 如 <time-formatted>;
  • customized built-in: 继承 HTMLButtonElement 等内置类, 复用原生语义与行为;

如何定义一个自定义元素并注册?​

class MyElement extends HTMLElement {
/* 生命周期回调, 全部可选 */
}

customElements.define("my-element", MyElement);

document.createElement("my-element"); // JS 中同样可创建
  • 效果: 页面上每个 <my-element> 都成为 MyElement 的实例, 并触发相应回调;
  • 注册前就存在的同名标签不是错误, 只是"未定义", 见下文升级;

生命周期回调在什么时候触发?​

回调触发时机
constructor()实例创建; 此时属性还没赋值
connectedCallback()元素被插入文档; 反复增删会反复调用
disconnectedCallback()元素从文档移除; 同样可多次调用
static observedAttributes返回需要监听的属性名数组
attributeChangedCallback(name, oldValue, newValue)被监听的属性发生变化
adoptedCallback()元素被移入新文档 (document.adoptNode), 极少用
  • 可选性: 只实现真正需要的回调即可;

未定义的元素如何升级?​

  • 选择器: 未定义元素可用 :not(:defined) 选中, 定义后变成 :defined;
  • 升级: 调用 customElements.define 时, 已存在的同名元素各自创建实例并触发 connectedCallback;
  • customElements.get(name): 返回该名字对应的类;
  • customElements.whenDefined(name): 返回一个在元素被定义后兑现 (无值) 的 Promise;

自定义内置元素如何扩展原生标签?​

  • 继承: class HelloButton extends HTMLButtonElement { /* ... */ };
  • 注册: 需要第三个参数指明扩展的标签, 因为多个标签可能共用一个 DOM 类;
  • 使用: HTML 里写普通标签再加 is 属性;
customElements.define("hello-button", HelloButton, { extends: "button" });
<button is="hello-button">Click me</button>
  • 收益: 保留原生样式、disabled 等既有行为, 搜索引擎与无障碍设备能识别其语义;

为什么渲染要放在 connectedCallback 而不是 constructor?​

  • constructor 太早: 浏览器还没处理属性, getAttribute 返回 null, 无法渲染;
  • 性能: 把工作推迟到真正需要时;
  • 时机语义: connectedCallback 在元素真正成为页面一部分时触发, 不只是被 append 到某个游离元素下;
  • 收益: 可以先离线构建 DOM、准备好元素, 等它进入页面才渲染;

属性变化如何触发重新渲染?​

  • 声明: 在 observedAttributes 中列出要监听的属性名;
  • 回调: 这些属性被修改时触发 attributeChangedCallback, 在其中重新渲染;
  • 性能: 未列出的属性不触发回调;
static get observedAttributes() { return ["datetime", "hour", "minute"]; }
attributeChangedCallback() { this.render(); }

解析顺序对 connectedCallback 中读取子节点有什么影响?​

  • 顺序: HTML 解析器先父后子, 父元素 connectedCallback 触发时子节点尚未创建;
  • 现象: 此时读 this.innerHTML 得到空字符串;
  • 传参: 优先用属性, 属性在元素创建时就可用;
  • 延迟方案: 用零延迟 setTimeout 等解析完成后再读, 但嵌套自定义元素会按外→内排队, 外层先完成初始化;
  • 现成回调: 没有"子元素就绪"回调; 需要时由内层派发 initialized 事件、外层监听响应;