一、核心理解:从“必须单根”到“允许多根”
背景变化:
- Vue 2 的限制:每个组件模板必须有一个单一的根元素包裹所有内容。
- Vue 3 的突破:组件可以直接返回多个平级的根节点,无需额外包装。
带来的新问题:
当组件返回多个平级节点(如 <div>A</div><p>B</p>)时,在底层渲染时需要一种方式来追踪和管理这一组节点——因为它们没有共同的父DOM元素作为“抓手”。
解决方案:
使用两个空文本节点作为锚点,像书签一样标记出这组节点的开始和结束位置。
简单来说:Vue 3 让你写模板时摆脱了单根限制,但底层实现上,为了能有效管理这些“散装”的节点,引入了锚点系统来标记它们的范围。
二、锚点的核心价值:在多Fragment场景下界定所有权
这是理解锚点必要性的最关键场景。假设一个容器内需要渲染多个组件,每个组件都是多根节点的Fragment:
<container>
<!-- 如果没有锚点,所有节点混杂在一起 -->
<div>A1</div> <!-- 属于哪个Fragment? -->
<p>B1</p> <!-- 属于哪个Fragment? -->
<div>A2</div> <!-- 属于哪个Fragment? -->
<p>B2</p> <!-- 属于哪个Fragment? -->
</container>
注:当多个组件(每个都是多根节点)一起渲染时,所有子节点混在一起,无法区分哪些节点属于哪个组件。
问题:无法区分节点归属,无法单独更新或删除某个Fragment。
<container>
<!-- 使用锚点后,范围清晰 -->
[空文本 start1] <!-- Fragment 1 开始 -->
<div>A1</div> <!-- 明确属于 Fragment 1 -->
<p>B1</p> <!-- 明确属于 Fragment 1 -->
[空文本 end1] <!-- Fragment 1 结束 -->
[空文本 start2] <!-- Fragment 2 开始 -->
<div>A2</div> <!-- 明确属于 Fragment 2 -->
<p>B2</p> <!-- 明确属于 Fragment 2 -->
[空文本 end2] <!-- Fragment 2 结束 -->
</container>
注:每个Fragment都有自己的一对锚点,锚点之间的节点就是这个Fragment的内容,这样就不会混淆了。
三、锚点的三大操作能力(持有引用即可)
只要保存了startAnchor和endAnchor的引用,你就拥有了对这个Fragment内节点的完整控制权。
1. 精准遍历与查找
// 遍历Fragment内所有子节点
// 从开始锚点的下一个兄弟节点开始,到结束锚点前结束
let node = startAnchor.nextSibling; // 获取第一个实际子节点
while (node !== endAnchor) { // 只要还没遇到结束锚点
console.log('当前节点:', node); // 处理这个节点
node = node.nextSibling; // 移动到下一个兄弟节点
}
// 注:这个循环能精准遍历Fragment内的所有子节点,不会跑到其他Fragment的节点
// 快速查找第N个子节点(例如找"a2")
function getFragmentChild(startAnchor, endAnchor, index) {
let current = startAnchor.nextSibling; // 从第一个子节点开始
let count = 0;
// 遍历直到找到第index个节点或遇到结束锚点
while (current !== endAnchor && count < index) {
current = current.nextSibling; // 移动到下一个节点
count++; // 计数
}
// 如果找到了就返回节点,否则返回null
return current !== endAnchor ? current : null;
}
// 注:要找Fragment里的第2个子节点,就调用getFragmentChild(startAnchor, endAnchor, 1)
// (因为索引从0开始,第2个节点的索引是1)
2. 安全的范围操作
-
在Fragment前插入新内容:
// 在Fragment开始锚点前插入新节点 container.insertBefore(newNode, startAnchor); // 注:这样插入的节点不会进入Fragment内部,而是在Fragment前面 -
删除整个Fragment:
// 先删除Fragment内的所有子节点 let node = startAnchor.nextSibling; // 从第一个实际子节点开始 while (node !== endAnchor) { // 直到遇到结束锚点 const next = node.nextSibling; // 先保存下一个节点 container.removeChild(node); // 删除当前节点 node = next; // 移动到下一个节点 } // 最后移除两个锚点节点本身 container.removeChild(startAnchor); container.removeChild(endAnchor); // 注:这样就完全清除了这个Fragment及其所有内容
3. 独立的存在与更新
因为每个Fragment的边界由自己的锚点唯一确定,所以:
-
可以独立挂载或卸载,不影响其他Fragment。
注:因为每个Fragment有自己的锚点边界,所以删除一个Fragment时,不会误删其他Fragment的节点。
-
更新时只需在自己的锚点范围内进行Diff,性能更高,更安全。
注:Vue的虚拟DOM diff算法只需要在startAnchor和endAnchor之间的节点中进行,不会扫描整个容器,效率更高。