不能直接 cloneNode(true) 克隆 template.content,因为它只复制节点结构而不保留表单控件的运行时状态(如 checked、selected、value、光标位置等),这是 DOM 规范行为而非 bug。

为什么不能直接 cloneNode(true) 克隆 template.content
因为 cloneNode(true) 只复制节点结构,不保留表单控件的运行时状态:<input checked> 克隆后变成未勾选,<select> 选中项重置为第一个选项,<textarea> 的光标位置和用户输入内容全丢。这不是浏览器 bug,而是 DOM 规范行为——cloneNode 不序列化可变状态。
常见误操作是写 template.innerHTML.replace(...) 或直接 el.innerHTML = template.innerHTML,这会破坏嵌套结构、丢失事件绑定、且极易引入 XSS(比如未转义的 data.title 插入到 innerHTML)。
-
template.innerHTML永远是空字符串,必须读template.content -
template.content是只读DocumentFragment,不能直接插入 DOM - 必须用
document.importNode(template.content, true)才能保活态
document.importNode(template.content, true) 的正确调用时机
必须在 DOMContentLoaded 之后执行,否则 template.content 可能为空(尤其模板在 <head> 里且 JS 同步执行时)。也不能在 window.load 之后才做——那只是延迟,不解决根本问题。
注意:这个 API 不是为 <template> “特供”的语法糖,而是语义保障——它把离线的 DocumentFragment 安全注入当前文档上下文,重置 ownerDocument,同时保留所有可序列化的 DOM 状态(包括 value、checked、selected)。
立即学习“前端免费学习笔记(深入)”;
- 必须显式传
true作为第二个参数,省略或传false会导致只克隆空 fragment - 返回节点的
parentNode为null,isConnected为false,需手动appendChild或insertBefore - 若模板含
<script>或<style>,它们会被克隆但不会自动执行或生效
怎么安全填充 data-bind 字段而不是用正则替换
模板里写 {{title}} 这类插值语法,原生 HTML 完全不识别;用 innerHTML.replace(/{{(\w+)}}/g, ...) 是危险且不可靠的——无法处理属性绑定(如 )、易被恶意数据绕过、不支持嵌套结构(如 <option value="{{id}}" selected>{{name}}</option>)。
推荐用 dataset 驱动绑定:<h2 data-bind="title"></h2>,然后遍历查找并赋值:
const fragment = document.importNode(tmpl.content, true);
fragment.querySelectorAll('[data-bind]').forEach(el => {
const key = el.dataset.bind;
if (data[key] != null) {
if (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') {
el.value = data[key];
} else if (el.hasAttribute('data-bind-attr')) {
el.setAttribute(el.dataset.bindAttr, data[key]);
} else {
el.textContent = data[key];
}
}
});
- 避免使用 ID 绑定(ID 冲突风险高),统一用
data-bind+dataset - 属性绑定需额外约定,比如
data-bind-attr="src"表示把值设为src属性 - 不要对整个 fragment 做
innerHTML = ...,会丢掉已绑定的事件监听器
高频复用时要不要缓存克隆结果
要,但得缓存对的地方。错误做法是 const cached = template.content —— 这只是个只读 fragment,不能插入 DOM;正确做法是缓存一次克隆结果,后续用它再克隆:
const base = document.importNode(template.content, true); // 后续每次: const instance = base.cloneNode(true); // 快,且仍保活态 // 填充 instance,再插入
这样比每次都 document.importNode(template.content, true) 快得多,尤其渲染 50+ 条列表时。但要注意:缓存的 base 是“干净实例”,不含任何业务数据,也不带事件监听器;填充时仍需逐个设置 el.value 或 el.textContent,不能依赖初始 value="default"。
批量插入务必用 DocumentFragment 中转,避免循环中反复触发重排:
const frag = document.createDocumentFragment();
for (let i = 0; i < list.length; i++) {
const instance = base.cloneNode(true);
// 填充 instance...
frag.appendChild(instance);
}
listContainer.appendChild(frag); // 一次性插入
真正容易被忽略的是:缓存后的 base 本身仍是脱离文档的节点,它的 querySelector 可用,但 getBoundingClientRect() 或 offsetHeight 返回 0;所有样式计算、布局测量都得等插入真实 DOM 后才生效。



















