Web Component-自定义元素的生命周期

551 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

在之前大概了解了 Web Component,知道是怎么玩的,不过在查阅资料后发现之前有些错误

Node.cloneNode 的局限性

在之前,使用的是template.content.cloneNode(true);来创建出一个新的节点,这种方法有一些局限性:会复制原节点的所有属性及属性值,其中就包括id,这样页面中就有多个id一样的元素了

拷贝它所有的属性以及属性值,当然也就包括了属性上绑定的事件(比如 onclick="alert(1)"),但不会拷贝那些使用 addEventListener()方法或者 node.onclick = fn 这种用 JavaScript 动态绑定的事件. --mdn

MDN 同样也给出警告

注意:为了防止一个文档中出现两个 ID 重复的元素,使用 cloneNode()方法克隆的节点在需要时应该指定另外一个与原 ID 值不同的 ID

而且,生成的节点的ownerDocument仍然指向源节点的ownerDocument

既然这个方法有一定的局限性,那么有没有更好的方法呢?有的!

document.importNode

document.importNode将外部文档的一个节点拷贝一份,然后可以把这个拷贝的节点插入到当前文档中

使用方式

// deep参数表示是否递归复制源节点的所有子节点
const node = document.importNode(externalNode, deep);

注意:

新生成节点的 parentNodenull,因为它还没有插入当前文档的文档树中,属于游离状态,因此无法对其进行操作

这样的话我们就可以很方便的修改新节点的ownerDocument(在append之后自动指向当前document

试试新的 api 来创建组件

<script>
  class MHeader extends HTMLElement {
    constructor() {
      super();
      // ...
      const content = document.importNode(template.content, true);
      /**
       * 注意,要先将节点插入到shadowRoot后才能对其进行操作
       */
      shadowRoot.appendChild(content);
      // ...
    }
  }
</script>

HTMLElement 的生命周期

在之前,我们都是将各种操作直接写在构造函数里,这显然有些臃肿,那么有没有一些办法可以将这些逻辑分发出去呢?有的!

HTMLElement内,有一些生命周期

  • connectedCallback,当 custom element 首次被插入文档 DOM 时被调用

  • disconnectedCallback,当 custom element从文档 DOM 中删除时被调用

  • adoptedCallback, 当 custom element被移动到新的文档时被调用

  • attributeChangedCallback,当 custom element增加、删除、修改自身属性时被调用

据此,我们可以重构一下之前的组件

<script>
  class MHeader extends HTMLElement {
    static get observedAttributes() {
      return ["title"];
    }
    constructor() {
      super();
      // 会自动向this上挂载一个shadowRoot
      this.attachShadow({ mode: "open" });
      const template = document.querySelector("#mHeaderTemplate");
      // const content = template.content.cloneNode(true);
      const content = document.importNode(template.content, true);
      this.shadowRoot.appendChild(content);
    }

    connectedCallback() {
      const loginBtn = this.shadowRoot.querySelector("#login");
      loginBtn.addEventListener("click", () => {
        this.setAttribute("title", "登录成功");
      });
    }

    attributeChangedCallback(name, oldValue, newValue) {
      if (name === "title") {
        this.shadowRoot.querySelector("#content").innerText = newValue;
      }
    }
  }
</script>

在这里,我们使用attributeChangedCallback来监听组件属性变化,在title属性发生变化时,更新元素的值。该回调会返回三个参数:

  • name,变化的属性的名字

  • oldValue,属性之前的值

  • newValue,要设置的值

(react 写的多了,有点梦回 vue 的感觉 🤔)

此外有一点要注意:

需要在static get observedAttributes函数里声明一下要监听的属性名

static get observedAttributes() {
  return ["title"];
}

完整代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>

  <body>
    <template id="mHeaderTemplate">
      <style>
        :host {
          font-size: 32px;
        }

        .content {
          color: #f00;
        }
      </style>
      <div>
        <span class="content" id="content">我是头部</span>
        <button id="login">login</button>
      </div>
    </template>

    <m-header id="mHeader" title="hello" />

    <script>
      class MHeader extends HTMLElement {
        static get observedAttributes() {
          return ["title"];
        }
        constructor() {
          super();
          // 会自动向this上挂载一个shadowRoot
          this.attachShadow({ mode: "open" });
          const template = document.querySelector("#mHeaderTemplate");
          // const content = template.content.cloneNode(true);
          const content = document.importNode(template.content, true);
          this.shadowRoot.appendChild(content);
        }

        connectedCallback() {
          const loginBtn = this.shadowRoot.querySelector("#login");
          loginBtn.addEventListener("click", () => {
            this.setAttribute("title", "登录成功");
          });
        }

        attributeChangedCallback(name, oldValue, newValue) {
          if (name === "title") {
            this.shadowRoot.querySelector("#content").innerText = newValue;
          }
        }
      }

      window.customElements.define("m-header", MHeader);

      const mHeader = document.querySelector("#mHeader");
    </script>
  </body>
</html>

总结

由于Node.cloneNode仍默认保留新节点的ownerDocument,因此使用起来没法放开,有些需要注意的地方,不过我们可以使用document.importNode来实现相同的功能,该API创建的节点处于游离状态,在进行append操作后会自动修正ownerDocument的指向

通过使用custom elements提供的生命周期函数,我们可以很方便地监听属性变化来做一些逻辑,但还是那句话书写起来有些不够便捷,需要用到原生 dom 操作 api,没准 jQuery 会借着 Web Components 再重新活跃起来

参考

MDN-Web Components

MDN/web-components-examples

Node.cloneNode(deep)

document.importNode

使用 custom elements