DocumentFragment 到底是什么
-
一个轻量级的 DOM 容器节点(
nodeType === 11
),实现Node
/EventTarget
,不在文档树中,插入页面时自己不会被挂上去,而是**把它的所有子节点“搬家”**到目标容器,然后 Fragment 变空。 -
创建方式:
document.createDocumentFragment()
(兼容最好);在多数现代浏览器new DocumentFragment()
也可用。 -
与几个相近概念的区别
- Element:真实页面元素,会参与样式/布局;Fragment 不会。
- ShadowRoot:是真实附着在宿主上的“局部 DOM 树”;Fragment 只是临时容器。
- template.content:是一个
DocumentFragment
,处于“惰性 / inert”状态(脚本、图片不执行/加载,直到被移入文档)。
-
事件与样式:Fragment 里构建时不触发渲染;你可以在节点上注册事件,但通常在挂载后再注册/委托更合理。
为什么要用 DocumentFragment
- 批量 DOM 更新更快、更稳
- DOM 操作昂贵;把许多节点先拼到一个“离线容器”里,再一次性插入文档,可显著减少布局/绘制与样式计算的次数,降低抖动和闪烁。
- 现代浏览器会对连续插入做一定合并,但一旦你在插入过程中读取布局信息(如
offsetWidth
)、或与其它同步布局操作交错,仍会触发多次回流;Fragment 能把这批变更“打包”。
- 原子性与语义性
- 先在内存中构建完整子树、校验完再一次性挂载:要么全部成功,要么不改动页面,避免中间状态被用户看到或被观察器捕获到多条松散记录。
- 与解析/模板很好地协作
<template>
的content
就是一个DocumentFragment
;Range.createContextualFragment()
也直接产出DocumentFragment
,适合把一段 HTML 字符串解析成节点,再一次性挂载。
- 减少观察器/事件干扰
- 构建阶段不在文档树里,
MutationObserver
不会被反复触发;最终插入通常只产生一次 childList 变更记录(addedNodes
为一组)。
小提醒:不是说 “不用 Fragment 就一定慢”。但在包含布局读取、复杂样式计算、或大量节点 的场景,用 Fragment 往往更可控、更稳。
如何高质量地用好 DocumentFragment
1) 大量插入:批量构建 + 一次挂载
const list = document.getElementById('list');
const frag = document.createDocumentFragment();
for (let i = 0; i < 5000; i++) {
const li = document.createElement('li');
li.textContent = `Item ${i}`;
frag.appendChild(li); // 先拼在离线容器
}
list.appendChild(frag); // 一次性挂载,frag 变空
与直接循环 append 的差异(含布局读取)
// ❌ 交替读写会迫使浏览器频繁同步布局
for (let i = 0; i < data.length; i++) {
const li = document.createElement('li');
li.textContent = data[i].name;
list.appendChild(li);
// 读取布局(强制回流)
if (list.offsetHeight > 10000) break;
}
// ✅ 先在 Fragment 里构建,再统一挂载再读
const frag = document.createDocumentFragment();
for (const item of data) {
const li = document.createElement('li');
li.textContent = item.name;
frag.appendChild(li);
}
list.appendChild(frag);
if (list.offsetHeight > 10000) { /* ... */ }
2) 用 <template>
+ Fragment 渲染列表(更可读)
<template id="row-tpl">
<li class="row">
<span class="name"></span>
<span class="price"></span>
</li>
</template>
<ul id="goods"></ul>
const tpl = document.getElementById('row-tpl');
const goods = document.getElementById('goods');
const frag = document.createDocumentFragment();
for (const item of items) {
const node = tpl.content.firstElementChild.cloneNode(true); // 克隆模板项
node.querySelector('.name').textContent = item.name;
node.querySelector('.price').textContent = `$${item.price}`;
frag.appendChild(node);
}
goods.appendChild(frag);
由于
template.content
是 Fragment,里面的脚本不会执行、图片不加载,直到你把这些节点移入文档。
3) 把 HTML 字符串解析成节点:Range.createContextualFragment
比直接 innerHTML=
更灵活,能在特定上下文里解析(如表格单元、列表项等)。
const html = `
<li class="msg"><strong>Info:</strong> Hello</li>
<li class="msg"><strong>Info:</strong> World</li>
`;
const ul = document.querySelector('#messages');
const range = document.createRange();
range.selectNode(ul); // 提供解析上下文(很重要)
const frag = range.createContextualFragment(html);
ul.appendChild(frag);
安全提示:
createContextualFragment
一样会解析并执行潜在的危险内容(如<img onerror=...>
)。不信任的 HTML 必须先做 XSS 过滤/清理。
4) 批量替换:replaceChildren
+ Fragment(原子更新)
function renderUsers(container, users) {
const frag = document.createDocumentFragment();
for (const u of users) {
const li = document.createElement('li');
li.textContent = `${u.id} — ${u.name}`;
frag.appendChild(li);
}
container.replaceChildren(frag); // 等价于把 frag 的子节点整体换上去
}
replaceChildren
能保证一次性替换内容,避免部分渲染的中间态。
5) 批量剪切/搬移 DOM:Range.extractContents
/ cloneContents
extractContents()
返回一个 DocumentFragment,同时把这段从文档中移除。cloneContents()
则是只复制。
// 把 <section id="a"> 到 <section id="b"> 之间的内容剪出来,插到 #target
const start = document.getElementById('a');
const end = document.getElementById('b');
const range = document.createRange();
range.setStartAfter(start);
range.setEndBefore(end);
const frag = range.extractContents(); // 文档里这段被移除了
document.getElementById('target').appendChild(frag);
6) MutationObserver:减少噪声
观察大容器时,使用 Fragment 一次性插入能把多次节点添加“聚合”为一次变更记录(实现依浏览器,但一般是一条 childList
,其中 addedNodes
包含全部新节点)。
const mo = new MutationObserver(records => {
// 通常这里能拿到一条记录,里面含多个 addedNodes
});
mo.observe(list, { childList: true });
// 用 Fragment 批量插入
const frag = document.createDocumentFragment();
// ... append many children
list.appendChild(frag);
7) 常见陷阱与最佳实践
- Fragment 插入后会被清空:以后还要复用?请先
frag.cloneNode(true)
(但克隆会复制一份,注意成本),或每次重新构建。 - 追加的是“搬家”不是“复制”:把已在文档中的节点 append 到 Fragment,会把它从原位置移走。
- 事件绑定的时机:多数情况下,挂载后用事件委托更简单稳妥(绑定在容器上,利用冒泡)。
- 脚本执行:通过 DOM API 创建并插入的
<script>
会按规则加载/执行;在<template>
中是惰性的,移入文档后才会按正常规则生效。 - 性能评估:用 Performance 面板或
performance.now()
/console.time()
实测,不要只凭感觉。
console.time('frag');
const frag = document.createDocumentFragment();
for (let i = 0; i < 10000; i++) {
const div = document.createElement('div');
div.textContent = i;
frag.appendChild(div);
}
container.appendChild(frag);
console.timeEnd('frag');
8) 什么时候我就不用 Fragment?
- 少量节点、且过程中不读取布局,有时直接
append
也足够快、代码更简单。 - 现代 API(如
element.append(node1, node2, ...)
)内部等效“批量”插入,写起来更短;不过显式使用 Fragment 能把“离线构建”的意图表达得更清楚,并便于做复杂拼装。
小结(可当作 Checklist)
- 需要批量构建/插入或避免中间态 → 用
DocumentFragment
。 - 解析 HTML →
Range.createContextualFragment
/<template>.content
→ Fragment。 - 整块替换 →
replaceChildren(frag)
。 - 大段搬移 →
Range.extractContents()
/cloneContents()
(返回 Fragment)。 - 记住:插入后 Fragment 为空,节点是被“搬家”的。