克隆的Custom HTML Element为何挂载主DOM后才升级为自定义元素
脱离主文档的自定义元素不触发升级的问题说明
复现场景
通过<template>标签存放自定义元素<custom-element>的模板,点击按钮时从模板克隆节点,给节点的自定义属性displayName赋值后再插入页面列表,会发现赋值时自定义setter不触发,只有节点插入主DOM后元素才会升级为自定义类实例。
核心复现代码:
class CustomElement extends HTMLElement { set displayName(v) { this.querySelector(".name").textContent = v; } } customElements.define("custom-element", CustomElement) const template = document.querySelector("template"); const list = document.querySelector(".list"); document.querySelector("button").addEventListener("click", () => { const el = template.content.firstElementChild.cloneNode(true); console.log("Cloned el constructor:", el.constructor.name); // 此时赋值不会触发自定义setter el.displayName = "Should has content"; const frag = new DocumentFragment(); frag.appendChild(el); console.log("Cloned el in DocumentFragment constructor:", el.constructor.name); // 插入主文档后元素才完成升级 list.appendChild(frag); console.log("Cloned el in Document constructor:", el.constructor.name); });
模板结构:
<template> <custom-element><span class="name"></span></custom-element> </template> <button>Add Item</button> <div class="list"></div>
点击按钮后控制台输出如下,可见元素只有插入主文档后构造函数才变为CustomElement:
Cloned el constructor: HTMLElement Cloned el in DocumentFragment constructor: HTMLElement Cloned el in Document constructor: CustomElement
核心原因
自定义元素的自动升级遵循浏览器DOM规范要求,只有关联到主文档(即当前页面document对象)节点树的元素,才会被浏览器识别并匹配已注册的自定义类完成升级:
- 存放在
<template>标签内部的节点,属于模板独立的文档片段上下文,不属于主文档树,不会被自动升级 - 从template直接克隆得到的节点、挂载到手动
new DocumentFragment()创建的独立片段上的节点,都没有接入主文档上下文,此时浏览器只会将其识别为普通HTMLElement实例,自定义类上定义的setter、生命周期方法都不会生效,给元素赋值属性只是给普通对象新增属性,不会触发自定义逻辑 - 只有当节点被直接或间接插入主文档DOM树(比如示例中执行
list.appendChild(frag))时,浏览器才会遍历检查插入的节点,匹配到已注册的自定义元素后完成原型替换,也就是控制台看到的构造函数变为CustomElement的升级过程。
之前调用customElements.upgrade(el)没有效果,是因为刚从template克隆出的节点还属于模板的独立文档上下文,不在主文档的自定义元素注册表作用范围内,调用升级方法自然不会生效。
挂载到可见DOM前完成初始化的可行方案
- 正确使用
customElements.upgradeAPI提前升级
先把克隆得到的节点挂载到和主文档关联的容器上(比如主文档创建的DocumentFragment),让节点接入主文档上下文,再调用升级方法,就可以在节点插入可见DOM前完成升级,修改后的代码如下:const el = template.content.firstElementChild.cloneNode(true); const frag = new DocumentFragment(); frag.appendChild(el); // 此时节点已关联主文档上下文,执行升级 customElements.upgrade(el); // 升级完成后赋值即可触发自定义setter el.displayName = "Should has content"; list.appendChild(frag); - 直接通过
createElement创建自定义元素
只要自定义元素已经完成注册,调用document.createElement('custom-element')创建的节点会在创建时直接升级为自定义类实例,不需要等待挂载到DOM树,可以直接配置属性,示例:// 替换从template克隆的逻辑 const el = document.createElement('custom-element'); el.innerHTML = '<span class="name"></span>'; // 直接赋值触发setter el.displayName = "Should has content"; list.appendChild(el); - 给自定义元素增加预赋值属性兼容逻辑
在自定义元素的构造函数中,检查升级前已经被赋值到实例上的普通属性,删除遮蔽原型的自有属性后重新赋值,触发自定义setter逻辑,兼容先赋值后挂载的场景,示例:class CustomElement extends HTMLElement { constructor() { super(); // 处理升级前预赋值的displayName属性 if (Object.prototype.hasOwnProperty.call(this, 'displayName')) { const preValue = this.displayName; delete this.displayName; this.displayName = preValue; } } set displayName(v) { this.querySelector(".name").textContent = v; } } customElements.define("custom-element", CustomElement)
内容的提问来源于stack exchange,提问作者Luke Vo
相关产品推荐
相关产品推荐

