将 Shadow DOM 附加到自定义元素可以消除错误,但为什么呢?

jhp*_*ING 2 html shadow-dom custom-element

根据自定义元素规范

该元素不得获得任何属性或子元素,因为这违反了使用createElementcreateElementNS方法的消费者的期望。

在这种情况下,Firefox 和 Chrome 都会正确地抛出错误。但是,当附加影子 DOM 时,不会出现错误(在任一浏览器中)。

火狐浏览器:

NotSupportedError:不支持操作

铬合金:

未捕获的 DOMException:无法构造“CustomElement”:结果不得有子级

没有影子 DOM

function createElement(tag, ...children) {
  let root;

  if (typeof tag === 'symbol') {
    root = document.createDocumentFragment();
  } else {
    root = document.createElement(tag);
  }

  children.forEach(node => root.appendChild(node));

  return root;
}

customElements.define(
  'x-foo',
  class extends HTMLElement {
    constructor() {
      super();

      this.appendChild(
        createElement(
          Symbol(),
          createElement('div'),
        ),
      );
    }
  },
);

createElement('x-foo');
Run Code Online (Sandbox Code Playgroud)

使用影子 DOM

function createElement(tag, ...children) {
  let root;

  if (typeof tag === 'symbol') {
    root = document.createDocumentFragment();
  } else {
    root = document.createElement(tag);
  }

  children.forEach(node => root.appendChild(node));

  return root;
}

customElements.define(
  'x-foo',
  class extends HTMLElement {
    constructor() {
      super();

      // it doesn't matter if this is open or closed
      this.attachShadow({ mode: 'closed' }).appendChild(
        createElement(
          Symbol(),
          createElement('div'),
        ),
      );
    }
  },
);

createElement('x-foo');
Run Code Online (Sandbox Code Playgroud)

请注意:为了查看示例,您需要(至少)使用以下设备之一:Firefox 63、Chrome 67、Safari 10.1。不支持边缘。

我的问题如下:

根据规范,所展示的行为是否正确?

向根添加子节点会导致 DOM 回流;如果没有影子 DOM,如何避免这种情况呢?

Int*_*lia 7

每次创建元素时都是通过构造函数完成的。但是,当调用构造函数时,没有子级也没有任何属性,这些都是在创建组件后添加的。

即使该元素是在 HTML 页面中定义的,它仍然是由使用构造函数的代码创建的,然后由解析 HTML 页面中 DOM 的代码添加属性和子元素。

调用构造函数时,没有子级,您无法添加它们,因为 DOM 解析器可能会在构造函数完成后立即添加它们。相同的规则适用于属性。

目前除了通过 JS 代码之外,无法指定 ShadowDOM 或 ShadowDOM 子元素。DOM 解析器不会向 ShadowDOM 添加任何子级。

因此,根据规范,在构造函数中访问、更改属性或子项或对其执行任何操作都是非法的。但是,由于 DOM 解析器无法将任何不非法的内容添加到组件 ShadowDOM 中。

当不使用 ShadowDOM 时,我通过使用在构造函数中创建的内部模板元素来解决这个问题,然后在调用时将其作为子元素放置connectedCallback

// Class for `<test-el>`
class TestEl extends HTMLElement {
  constructor() {
    super();
    console.log('constructor');
    const template = document.createElement('template');
    template.innerHTML = '<div class="name"></div>';
    this.root = template.content;
    this.rendered = false;
  }

  static get observedAttributes() {
    return ['name'];
  }

  attributeChangedCallback(attrName, oldVal, newVal) {
    if (oldVal !== newVal) {
      console.log('attributeChangedCallback', newVal);
      this.root.querySelector('.name').textContent = newVal;
    }
  }

  connectedCallback() {
    console.log('connectedCallback');
    if (!this.rendered) {
      this.rendered = true;
      this.appendChild(this.root);
      this.root = this;
    }
  }

  // `name` property
  get name() {
    return this.getAttribute('name');
  }
  set name(value) {
    console.log('set name', value);
    if (value == null) { // Check for null or undefined
      this.removeAttribute('name');
    }
    else {
      this.setAttribute('name', value)
    }
  }
}

// Define our web component
customElements.define('test-el', TestEl);

const moreEl = document.getElementById('more');
const testEl = document.getElementById('test');
setTimeout(() => {
testEl.name = "Mummy";
  const el = document.createElement('test-el');
  el.name = "Frank N Stein";
  moreEl.appendChild(el);
}, 1000);
Run Code Online (Sandbox Code Playgroud)
<test-el id="test" name="Dracula"></test-el>
<hr/>
<div id="more"></div>
Run Code Online (Sandbox Code Playgroud)

此代码在构造函数中创建一个模板并用于this.root引用它。一旦connectedCallback被调用,我将模板插入到 DOM 中并更改this.root为指向this,以便我对元素的所有引用仍然有效。

这是一种快速方法,可以让您的组件始终能够保持其子组件的正确性,而无需使用 ShadowDOM,并且仅在connectedCalback调用一次时将模板作为子组件放入 DOM 中。