这是一个小提示,显示Chrome 72和Firefox 63中的控制台错误:
https://jsfiddle.net/jr2z1ms3/1/
代码是:
<script>
customElements.define('test-element', class extends HTMLElement {
constructor() {
super()
Promise.resolve().then(() => {
this.setAttribute('foo', 'bar')
})
}
})
</script>
<test-element>test</test-element>
在Chrome中,错误是:
Uncaught DOMException: Failed to construct 'CustomElement': The result must not have attributes
在Firefox中,错误是:
NotSupportedError: Operation is not supported
如果您对setAttribute
调用发表评论,则两个浏览器中的错误都会消失。
以下示例说明了在连接元素之前更改属性,这表明可以使用macrotasks完成,但(不公平地)不使用微任务:
(working fiddle下面的代码段)
customElements.define('test-element', class extends HTMLElement {
constructor() {
super()
setTimeout(() => {
this.setAttribute('foo', 'bar')
})
}
connectedCallback() {
console.log('foo attribute:', this.getAttribute('foo'))
}
})
const el = document.createElement('test-element')
console.log('no foo attribute:', el.getAttribute('foo'))
setTimeout(() => {
document.body.appendChild(el)
})
在第一个例子中,我没有在构造函数中设置属性,我推迟到未来的微任务。那么为什么浏览器会抱怨?如果这是按照规范打算的,那么规范是否有“设计错误”?我们为什么不能这样做?
基于下面的答案,我不明白为什么需要这个限制。一个糟糕的开发人员仍然可以使用或不使用此浏览器引擎限制。
IMO,让开发人员决定(并记录)他们的自定义元素如何工作。
如果我们能够在构造函数之后设置属性或构造函数之后的微任务,那么浏览器是否存在一些技术限制?
根据spec,你必须在构造函数中做一些事情:
在创作自定义元素构造函数时,作者受以下一致性要求的约束:
- 对super()的无参数调用必须是构造函数体中的第一个语句,以便在运行任何其他代码之前建立正确的原型链和此值。
- return语句不能出现在构造函数体内的任何位置,除非它是一个简单的早期返回(返回或返回此)。
- 构造函数不得使用document.write()或document.open()方法。
- 不得检查元素的属性和子元素,因为在非升级的情况下不会出现任何属性和子元素,并且依赖于升级会使元素变得不可用。
- 元素不得获得任何属性或子元素,因为这违反了使用createElement或createElementNS方法的消费者的期望。
- 通常,应尽可能将工作推迟到connectedCallback - 尤其是涉及获取资源或呈现的工作。但是,请注意,connectedCallback可以被多次调用,因此任何真正一次性的初始化工作都需要一个防护来防止它运行两次。
- 通常,构造函数应该用于设置初始状态和默认值,以及设置事件侦听器和可能的影子根。
在元素创建期间,直接或间接检查其中一些要求,如果不遵循它们,将导致无法通过解析器或DOM API实例化的自定义元素。
您的示例的问题是Promise
立即被解析,因此仍然在构造函数中。
如果您将代码更改为:
customElements.define('test-element', class extends HTMLElement {
constructor() {
super()
setTimeout(() => {
this.setAttribute('foo', 'bar')
}, 100)
}
})
<test-element>test</test-element>
然后它工作,因为setTimeout
让你离开构造函数。
spec提到这个:
即使工作在构造函数启动的微任务中完成,也是如此,因为微任务检查点可以在构造之后立即发生。