JavaScript自定义元素循环渲染重复内容原因排查(ShadowDOM、Template)
问题分析与解决
问题描述
你编写了基于JavaScript类、ShadowDOM和HTML Template的自定义元素代码,预期渲染结果为carrot blueberry,但实际输出为carrot carrot,循环未按预期执行。
原代码
JavaScript部分
class Product extends HTMLElement { connectedCallback() { let products = [ { name: "carrot", price: "$ 6.99" }, { name: "blueberry", price: "$ 6.99" } ]; let pro = document.querySelector("#product_item").content; let copyHTML = document.importNode(pro, true); for (let product of products) { copyHTML.querySelector(".cerealName").textContent = product.name; this.attachShadow({ mode: "open" }); this.shadowRoot.append(copyHTML.cloneNode(true)); } } } customElements.define("product-item", Product);
HTML部分
<main> <product-item></product-item> <product-item></product-item> </main> <template id="product_item"> <li class="cereal"> <p class="cerealName" style="font-size: 3rem"></p> </li> </template>
核心原因
- Shadow DOM 重复创建异常:每个自定义元素只能创建一个Shadow DOM,你在循环内部调用
this.attachShadow({ mode: "open" }),第一次调用成功创建后,第二次调用会抛出DOMException(可在浏览器控制台查看),导致循环后续逻辑中断。 - 自定义元素的重复执行:HTML中存在两个
<product-item>标签,每个标签的connectedCallback都会完整遍历一次products数组。即使Shadow DOM能重复创建,每个元素也会渲染两个产品,而非一个元素对应一个产品。 - 模板复用逻辑错误:你在循环外提前克隆了模板内容到
copyHTML,后续循环修改的是同一个copyHTML对象的内容。但由于Shadow DOM创建异常,第二次修改blueberry的逻辑根本没机会执行,最终每个元素只渲染了第一次循环的carrot。
修正方案
方案一:单个元素渲染所有产品
调整自定义元素为产品列表容器,一次渲染所有产品:
class ProductList extends HTMLElement { connectedCallback() { const products = [ { name: "carrot", price: "$ 6.99" }, { name: "blueberry", price: "$ 6.99" } ]; // 仅创建一次Shadow DOM this.attachShadow({ mode: "open" }); const template = document.querySelector("#product_item").content; for (const product of products) { // 每次循环克隆新的模板实例 const item = document.importNode(template, true); item.querySelector(".cerealName").textContent = product.name; this.shadowRoot.appendChild(item); } } } customElements.define("product-list", ProductList);
对应HTML:
<main> <product-list></product-list> </main> <template id="product_item"> <li class="cereal"> <p class="cerealName" style="font-size: 3rem"></p> </li> </template>
方案二:单个元素对应单个产品
通过属性传递产品数据,每个<product-item>渲染一个产品:
class ProductItem extends HTMLElement { // 监听属性变化 static get observedAttributes() { return ["name"]; } connectedCallback() { this.attachShadow({ mode: "open" }); const template = document.querySelector("#product_item").content; const item = document.importNode(template, true); item.querySelector(".cerealName").textContent = this.getAttribute("name"); this.shadowRoot.appendChild(item); } // 属性更新时同步内容 attributeChangedCallback(name, oldValue, newValue) { if (this.shadowRoot && name === "name") { this.shadowRoot.querySelector(".cerealName").textContent = newValue; } } } customElements.define("product-item", ProductItem);
对应HTML:
<main> <product-item name="carrot"></product-item> <product-item name="blueberry"></product-item> </main> <template id="product_item"> <li class="cereal"> <p class="cerealName" style="font-size: 3rem"></p> </li> </template>
内容的提问来源于stack exchange,提问作者moeyg
相关产品推荐
相关产品推荐

