将阴影 DOM 附加到自定义元素可以消除错误,但为什么呢?
Attaching shadow DOM to a custom element removes error, but why?
根据 custom element specification、
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement
or createElementNS
methods.
Firefox 和 Chrome 在这种情况下都会正确抛出错误。但是,附加阴影时 DOM,没有错误(在任一浏览器中)。
火狐:
NotSupportedError: Operation is not supported
Chrome:
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
无阴影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');
有阴影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');
请注意:为了查看示例,you need to be using(至少)以下之一:Firefox 63、Chrome 67、Safari 10.1。不支持边缘。
我的问题如下:
根据规范,行为是否正确?
将子节点添加到根节点会导致 DOM 回流;如果没有阴影 DOM 如何避免这种情况?
每次创建元素都是通过构造函数完成的。但是,当调用构造函数时,没有子项也没有任何属性,这些都是在创建组件后添加的。
即使该元素是在 HTML 页面中定义的,它仍然是由代码使用构造函数创建的,然后由解析 DOM 中的代码添加属性和子元素HTML 页。
调用构造函数时没有子项,您不能添加它们,因为 DOM 解析器可能会在构造函数完成后立即添加它们。相同的规则适用于属性。
目前无法指定 shadowDOM 或 shadowDOM children,除非通过 JS 代码。 DOM 解析器不会将任何子项添加到影子 DOM。
因此根据规范,在构造函数中访问、更改属性或子项或对其执行任何操作都是非法的。但是,由于 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);
<test-el id="test" name="Dracula"></test-el>
<hr/>
<div id="more"></div>
此代码在构造函数中创建一个模板并使用 this.root
引用它。
调用 connectedCallback
后,我将模板插入 DOM 并将 this.root
更改为指向 this
,这样我对元素的所有引用仍然有效。
这是一种让您的组件始终能够保持其子组件正确的快速方法,而无需使用阴影DOM并且仅将模板作为子组件放入DOM一次connectedCalback
被调用。
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
createElement()
的 "expectations" 将被赋予一个空元素(没有 HTML 属性,或子 HTML 元素),就像任何其他标准 HTML 使用 createElement()
创建的元素。
因此,将自定义元素添加到 HTML 和 DOM 规范(因此,对 HTML 引擎实现)的影响在某种程度上是有限的。
此限制不适用于 Shadow DOM,因为它之前不是规格的一部分。这不会改变上述预期。因此 "odd" 普通 DOM 树和阴影 DOM 树之间的差异,你是对的。
此外,能够在 contructor()
中添加阴影 DOM 而不是光源 DOM,确保添加光源 DOM 的元素时,它们将根据 Shadow DOM 模板(和事件模型,如果您收听 slotchange
事件)进行预防性过滤。
根据 custom element specification、
The element must not gain any attributes or children, as this violates the expectations of consumers who use the
createElement
orcreateElementNS
methods.
Firefox 和 Chrome 在这种情况下都会正确抛出错误。但是,附加阴影时 DOM,没有错误(在任一浏览器中)。
火狐:
NotSupportedError: Operation is not supported
Chrome:
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children
无阴影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');
有阴影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');
请注意:为了查看示例,you need to be using(至少)以下之一:Firefox 63、Chrome 67、Safari 10.1。不支持边缘。
我的问题如下:
根据规范,行为是否正确?
将子节点添加到根节点会导致 DOM 回流;如果没有阴影 DOM 如何避免这种情况?
每次创建元素都是通过构造函数完成的。但是,当调用构造函数时,没有子项也没有任何属性,这些都是在创建组件后添加的。
即使该元素是在 HTML 页面中定义的,它仍然是由代码使用构造函数创建的,然后由解析 DOM 中的代码添加属性和子元素HTML 页。
调用构造函数时没有子项,您不能添加它们,因为 DOM 解析器可能会在构造函数完成后立即添加它们。相同的规则适用于属性。
目前无法指定 shadowDOM 或 shadowDOM children,除非通过 JS 代码。 DOM 解析器不会将任何子项添加到影子 DOM。
因此根据规范,在构造函数中访问、更改属性或子项或对其执行任何操作都是非法的。但是,由于 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);
<test-el id="test" name="Dracula"></test-el>
<hr/>
<div id="more"></div>
此代码在构造函数中创建一个模板并使用 this.root
引用它。
调用 connectedCallback
后,我将模板插入 DOM 并将 this.root
更改为指向 this
,这样我对元素的所有引用仍然有效。
这是一种让您的组件始终能够保持其子组件正确的快速方法,而无需使用阴影DOM并且仅将模板作为子组件放入DOM一次connectedCalback
被调用。
The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods.
createElement()
的 "expectations" 将被赋予一个空元素(没有 HTML 属性,或子 HTML 元素),就像任何其他标准 HTML 使用 createElement()
创建的元素。
因此,将自定义元素添加到 HTML 和 DOM 规范(因此,对 HTML 引擎实现)的影响在某种程度上是有限的。
此限制不适用于 Shadow DOM,因为它之前不是规格的一部分。这不会改变上述预期。因此 "odd" 普通 DOM 树和阴影 DOM 树之间的差异,你是对的。
此外,能够在 contructor()
中添加阴影 DOM 而不是光源 DOM,确保添加光源 DOM 的元素时,它们将根据 Shadow DOM 模板(和事件模型,如果您收听 slotchange
事件)进行预防性过滤。