You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

核心原因

  1. Shadow DOM 重复创建异常:每个自定义元素只能创建一个Shadow DOM,你在循环内部调用this.attachShadow({ mode: "open" }),第一次调用成功创建后,第二次调用会抛出DOMException(可在浏览器控制台查看),导致循环后续逻辑中断。
  2. 自定义元素的重复执行:HTML中存在两个<product-item>标签,每个标签的connectedCallback都会完整遍历一次products数组。即使Shadow DOM能重复创建,每个元素也会渲染两个产品,而非一个元素对应一个产品。
  3. 模板复用逻辑错误:你在循环外提前克隆了模板内容到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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 16:25:16