首页 > 解决方案 > 自定义元素构造函数中的延迟 setAttribute 调用导致 DOM 错误。它是一个错误吗?

问题描述

这是一个在 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调用发表评论,则两个浏览器中的错误都会消失。

以下示例说明了在连接元素之前更改属性,这表明可以使用宏任务完成,但(不公平地)不能使用微任务:

(以下片段的工作小提琴)

    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,让开发人员决定(并记录)他们的自定义元素如何工作。

如果我们能够在构造函数或构造函数之后的微任务中设置属性,是否存在浏览器无法克服的一些技术限制?

标签: javascripthtmlweb-componentcustom-element

解决方案


根据规范,在构造函数中绝对不能做某些事情:

在创作自定义元素构造函数时,作者受以下一致性要求的约束:

  • 对 super() 的无参数调用必须是构造函数主体中的第一条语句,以在运行任何进一步的代码之前建立正确的原型链和该值。

  • return 语句不得出现在构造函数主体内的任何位置,除非它是简单的提前返回(return 或 return this)。

  • 构造函数不得使用 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让你脱离了构造函数。


推荐阅读