DOMParser解析的自定义元素挂载前无法触发setter的解决方法
问题复现
以下是自定义元素实现的复现代码:
class TestElement extends HTMLElement { backingField; get property() { return this.backingField; } set property(value) { this.backingField = value; console.log("Inside Setter"); } constructor() { super(); const template = document.createElement("template"); template.innerHTML = "<input>"; this.attachShadow({ mode: "open" }); this.shadowRoot.appendChild(template.content.cloneNode(true)); } } window.customElements.define("test-element", TestElement); // 通过HTML字符串解析获取元素 const element = new DOMParser().parseFromString("<test-element></test-element>", "text/html").body.childNodes[0]; // 尝试设置属性 element.property = "test"; document.body.appendChild(element);
执行上述代码时,element.property = "test" 赋值操作不会触发类中定义的setter,控制台不会打印预期的Inside Setter日志。
已知两种正常触发setter的场景:
- 使用
document.createElement("test-element")创建元素时,setter可正常触发 - 先将解析得到的元素追加到
document.body,再执行属性赋值,setter可正常触发,对应代码如下:
document.body.appendChild(element); element.property = "test";
需求为:必须通过HTML字符串解析的方式获取元素,且需要在元素追加到文档前完成属性赋值,正常触发setter逻辑。
问题原因
问题核心出在DOMParser的解析机制和自定义元素的升级规则:
DOMParser.parseFromString会生成一个完全独立的新Document上下文,该上下文有独立的window对象和自定义元素注册表,主页面通过window.customElements.define注册的TestElement,不会同步到这个独立Document的注册表中。- 因此解析得到的
<test-element>初始只是独立Document下的普通HTMLUnknownElement实例,原型链上不存在定义的property存取器,此时执行element.property = "test"只是给元素实例添加了一个名为property的自有属性,自然不会触发setter。 - 当元素被追加到主页面文档时,浏览器检测到该标签属于主文档已注册的自定义元素,才会执行升级流程:将元素原型替换为
TestElement.prototype,执行类的constructor。但此时元素上已经存在同名的自有property属性,根据JS原型链优先级规则,自有属性会遮盖原型上的存取器,后续对property的读写都会直接操作自有属性,不会走到定义的setter逻辑。 - 如果先追加元素再赋值,升级流程会先执行,此时元素实例上还没有同名自有属性,赋值操作就会正常走到原型上的setter。
解决方案
两种方案均可实现在元素追加到文档前正常触发setter,按需选择即可:
方案1:使用主文档上下文解析HTML字符串
替换DOMParser逻辑,使用主文档下的template元素解析HTML,解析过程直接复用主文档的自定义元素注册表,解析得到的元素从一开始就是完成升级的TestElement实例,无需等待插入文档。
// 主文档上下文解析HTML的工具方法 const parseHTML = (htmlString) => { const tpl = document.createElement('template'); tpl.innerHTML = htmlString.trim(); return tpl.content.firstElementChild; }; const element = parseHTML("<test-element></test-element>"); // 此时元素已完成升级,赋值直接触发setter element.property = "test"; // 控制台正常打印Inside Setter document.body.appendChild(element);
方案2:主动触发自定义元素升级
如果必须使用DOMParser解析,可以在拿到元素后、设置属性前,调用主文档的customElements.upgrade方法主动触发元素升级,升级完成后再赋值即可正常触发setter。
const doc = new DOMParser().parseFromString("<test-element></test-element>", "text/html"); const element = doc.body.firstElementChild; // 主动升级元素为TestElement实例 window.customElements.upgrade(element); // 升级后赋值正常触发setter element.property = "test"; // 控制台正常打印Inside Setter document.body.appendChild(element);
注意:不要在元素升级前给实例设置任何和类中定义的属性/存取器同名的自有属性,否则升级后自有属性会遮盖原型上的对应逻辑,导致异常。
内容的提问来源于stack exchange,提问作者Michael Haddad
相关产品推荐
相关产品推荐

