自定义元素构造函数何时调用? (HTMLTemplateElement.content 个问题)

When is a Custom Element constructor invoke? (HTMLTemplateElement.content problem)

看看这个简单的例子 (不要理会这个,去编辑)

class MyElement extends HTMLElement {

  customProperty = "something";

  constructor() {
    super();

    console.log("My Element Constructor");
  }
}

customElements.define("my-element", MyElement);

document.body.innerHTML = "<my-element></my-element>";
var myElement = document.querySelector("my-element");
console.log(myElement); // 
console.log(myElement.customProperty); //

输出:

<my-element>
undefined
My Element Constructor

在主调用堆栈完成之前,不会调用自定义元素构造函数(但会调用 HTMLElement 构造函数)。这是预期行为还是错误?

谢谢!

编辑

为了简化我的真实案例,我提出了前面的例子,我现在认为它不适合说明我的问题(这个例子确实有效)。感谢@connexo 的回答,我能够隔离问题(这是一个具有许多依赖项的复杂项目)并将其转换为以下示例:

class SubElement extends HTMLElement {

    constructor() {
        super();

        console.log("Sub Element Constructor");
    }
}

customElements.define("sub-element", SubElement);

class MainElement extends HTMLElement {

    constructor() {
        super();

        this.attachShadow({mode: "open"});

        let template = document.createElement("template");
        
        template.innerHTML = "<sub-element></sub-element>";

        this.shadowRoot.appendChild(template.content);

        // The above line doesn't call the Sub Element constructor
        // But the bellow line does it

        // this.shadowRoot.innerHTML = "<sub-element></sub-element>";

        console.log("Main Element Constructor");
    }
}

customElements.define("main-element", MainElement);

var myElement = document.createElement("main-element");

// Sub Component constructor will not be called until
// the parent element is added to the DOM (next line)
// document.body.appendChild(myElement);

由于某些原因,当 contenttemplate 元素被添加到 shadowRoot 时,这些元素将不会被解析,直到shadowRoot 添加到 DOM。 但是当直接修改shadowRoot的innerHTML时,即使shadowRoot不属于DOM.

也会解析这段内容

感谢您的宝贵时间。

问:什么时候调用自定义元素构造函数?

答:分三种情况:

  1. 如果您的元素在 元素被解析之前在自定义元素注册表中注册 ,则在解析 <my-element 部分时会调用其构造函数HTML。请注意缺少的 > - 在这种情况下,这是解析器在调用构造函数时解析的所有内容 - 无属性,无子项).

  2. 如果元素在解析之前尚未在自定义元素注册表中注册,则在自定义元素注册完成后立即调用构造函数.这称为 upgrade 案例。升级前,你的自定义元素到浏览器只是一个HTMLUnknownElement.

  3. 当您使用 new MyElement()document.createElement('my-element') 动态创建自定义元素时,也会调用构造函数。请注意,a) 在这两种情况下,属性和子元素都不存在,并且 b) 使用 document.createElement('my-element') 创建元素时,调用构造函数的时间点将是 1. 或 2.(取决于自定义元素是否已经是否注册过)。

只要您确保它不会在 document.body 可用之前 运行,您的代码就会按预期工作。

实现此目的的最简单方法是仅在结束 </body> 标记之前包含您的脚本。

如果您的 JS 在外部文件中,一个简单的替代方法是使用布尔属性 defer:

包含它
<script src="./path/to/my/script.js" defer></script>

第二种选择是将您的代码包装在 DOMContentLoaded 侦听器中(这样,放置 script 标记的位置并不重要,您也不需要 defer 属性):

document.addEventListener('DOMContentLoaded', function() {
    class MyElement extends HTMLElement {
    
      customProperty = "something";
    
      constructor() {
        super();
    
        console.log("My Element Constructor");
      }
    }
    
    customElements.define("my-element", MyElement);
    
    document.body.innerHTML = "<my-element></my-element>";
    var myElement = document.querySelector("my-element");
    console.log(myElement); // 
    console.log(myElement.customProperty); //

});

所有这三种方法基本上都强制执行升级案例 (2.),在多年创作大型 Web 组件库的专业实践中,这已被证明是最可靠和最简单的方法。

编辑

既然你现在已经完全改变了你的问题,这里就是答案:

The <template> HTML element is a mechanism for holding HTML that is not to be rendered immediately when a page is loaded but may be instantiated subsequently during runtime using JavaScript.

Think of a template as a content fragment that is being stored for subsequent use in the document. While the parser does process the contents of the <template> element while loading the page, it does so only to ensure that those contents are valid; the element's contents are not rendered, however.

https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template

However, the HTMLTemplateElement has a content property, which is a read-only DocumentFragment containing the DOM subtree which the template represents. Note that directly using the value of the content could lead to unexpected behavior, see Avoiding DocumentFragment pitfall section below.
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template#attributes

一个 <template> 没有任何关系

innerHTMLsynchronous,添加内容为append(child)A-synchronous

所以在下面的例子中,

  • .innerHTML 立即解析

  • 执行constructor的余数;
    之后 append 内容被解析。

  • 使用 <style onload> 事件显示代码何时被解析

  • 2 <div>s 检查 lightDOM (this.children.length) 何时被解析

<script>
customElements.define("sub-element", class extends HTMLElement {
  constructor() {
    super();console.log("Sub Element Constructor");
  }
  connectedCallback(){
  let maindivcount = () => this.parentNode.children.length;
  console.log("connectedCallback",this,maindivcount());
    setTimeout(()=>console.log("parsed!",this,maindivcount()));
  }
});
customElements.define("main-element", class extends HTMLElement {
  constructor() {
    console.log("Main Element Constructor START");

    let style = Object.assign(document.createElement("style"), {
      onload: () => console.log("<style> loaded in ", this,this.children.length)
    });
    super().attachShadow({mode:"open"})
           .innerHTML = "<sub-element></sub-element>"; // SYNCHRONOUS!

    this.shadowRoot.append(style); // A-SYNC!!

    console.log("Main Element Constructor END");
  }
  connectedCallback(){
    console.log("connectedCallback",this,this.children.length);
    setTimeout(()=>console.log("parsed!",this,this.children.length));
  }
});
</script>

<main-element><div></div><div></div></main-element>

createElement

customElements.define("sub-element", class extends HTMLElement {
  constructor() {
    super();
    console.log("Sub Element Constructor");
  }
});

customElements.define("main-element", class extends HTMLElement {
  constructor() {
    console.log("Main Element Constructor START");

    let style = Object.assign(document.createElement("style"), {
      onload: () => console.log("<style> loaded in ", this)
    });

    super().attachShadow({mode:"open"})
           .innerHTML = "<sub-element></sub-element>"; // SYNCHRONOUS!

    this.shadowRoot.append(style); // A-SYNC!!

    console.log("Main Element Constructor END");
  }
});

var myElement = document.createElement("main-element");
console.log("before append");
document.body.append(myElement);

我 select 更正了 @connexo 对解释的全面性的回答。但这是我解决问题的方法。 如果您需要复制模板元素及其内容 属性 的功能,但还需要 HTML 解析的内容直接实例化其中的自定义元素,请使用下一个方法:

function HTMLToFragment(html) {

    let divTemplate = document.createElement("div");
    divTemplate.innerHTML = html;

    let fragment = new DocumentFragment();

    fragment.prepend(...divTemplate.childNodes);

    return fragment;

}

在这里,使用这种技术,我的问题的解决方案:

class SubElement extends HTMLElement {

    constructor() {
        super();

        console.log("Sub Element Constructor");
    }
}

customElements.define("sub-element", SubElement);

class MainElement extends HTMLElement {

    constructor() {
        super();

        this.attachShadow({mode: "open"});

        let divTemplate = document.createElement("div");
        divTemplate.innerHTML = "<sub-element></sub-element>";

        let fragment = new DocumentFragment();

        fragment.prepend(...divTemplate.childNodes);                

        this.shadowRoot.append(fragment);

        console.log("Main Element Constructor");
    }
}

customElements.define("main-element", MainElement);

var myElement = document.createElement("main-element");