在前面的文章中,我们深入探讨了 Diff 算法和组件渲染的完整过程。今天,我们将聚焦于 Vue3 中两个特殊的节点类型——
Fragment和Portal/Teleport。它们打破了传统 DOM 树的限制,带来了更灵活的渲染能力。理解它们的实现原理,将帮助我们更好地掌握 Vue3 的渲染系统。
前言:打破DOM树的限制
在 Vue2 中,组件的模板必须有且只有一个根节点:
<template>
<!-- Vue2:必须有一个根元素 -->
<div>
<header>...</header>
<main>...</main>
<footer>...</footer>
</div>
</template>
这个限制在某些场景下很不方便,而且相当于给所有元素无形套上了一层父节点。Vue3 引入了两个新特性来解决这个问题:
-
Fragment:可以没有根节点(也可以说:允许有多个根节点):<template> <!-- 没有根节点(或者说有多个根节点) --> <header>...</header> <main>...</main> <footer>...</footer> </template> -
Teleport:可以把内容渲染到任何地方:<template> <button @click="open">打开模态框</button> <!-- 把内容渲染到任何地方,如:body --> <Teleport to="body"> <div class="modal">模态框内容</div> </Teleport> </template>
Fragment:多根节点的实现
Fragment的本质
Fragment 在 Vue3 中是一个特殊的 Symbol,它代表一组没有父容器的节点:
// Fragment的类型标识
const Fragment = Symbol('Fragment');
// Fragment的VNode结构
const fragmentVNode = {
type: Fragment, // 特殊类型
children: [ // 子节点数组
{ type: 'h1', children: '标题' },
{ type: 'p', children: '段落1' },
{ type: 'p', children: '段落2' }
],
shapeFlag: ShapeFlags.FRAGMENT | ShapeFlags.ARRAY_CHILDREN,
el: null, // 指向第一个子节点的el
anchor: null // 指向最后一个子节点的el
};
Fragment的渲染过程
Fragment 的渲染与普通元素完全不同:它不创建自己的 DOM 元素,而是直接渲染子节点:
function processFragment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 首次挂载
mountFragment(newVNode, container, anchor);
} else {
// 更新
patchFragment(oldVNode, newVNode, container, anchor);
}
}
function mountFragment(vnode, container, anchor) {
const { children, shapeFlag } = vnode;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本子节点:挂载为文本节点
const textNode = document.createTextNode(children);
vnode.el = textNode;
vnode.anchor = textNode;
insert(textNode, container, anchor);
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组子节点:挂载所有子节点
mountChildren(children, container, anchor);
// 设置el和anchor指向第一个和最后一个子节点
vnode.el = children[0]?.el;
vnode.anchor = children[children.length - 1]?.el;
}
}
function mountChildren(children, container, anchor) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
patch(null, child, container, anchor);
}
}
Fragment的更新策略
Fragment 的更新需要特殊处理,因为它没有自己的 DOM 元素:
function patchFragment(oldVNode, newVNode, container, anchor) {
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
// Fragment本身没有DOM,直接patch子节点
patchChildren(oldVNode, newVNode, container);
// 更新el和anchor引用
if (Array.isArray(newChildren)) {
newVNode.el = newChildren[0]?.el || oldVNode.el;
newVNode.anchor = newChildren[newChildren.length - 1]?.el || oldVNode.anchor;
}
}
// 在patchChildren中处理Fragment子节点
function patchChildren(oldVNode, newVNode, container) {
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
// 如果父节点是Fragment,使用特殊的锚点
const parentIsFragment = oldVNode.type === Fragment || newVNode.type === Fragment;
const anchor = parentIsFragment ? null : getAnchor(oldVNode);
if (Array.isArray(oldChildren) && Array.isArray(newChildren)) {
// 核心diff
patchKeyedChildren(oldChildren, newChildren, container);
} else {
// 其他情况的处理...
}
}
Fragment 的 DOM 范围管理
Fragment 需要管理一组连续的 DOM 节点,这是通过 el 和 anchor 实现的:
class FragmentNode {
constructor(children) {
this.children = children;
this.el = null; // 第一个DOM节点
this.anchor = null; // 最后一个DOM节点
}
// 插入整个Fragment
insert(container, refNode) {
if (!this.children.length) return;
// 插入第一个节点
container.insertBefore(this.children[0].el, refNode);
// 依次插入后续节点
for (let i = 1; i < this.children.length; i++) {
container.insertBefore(this.children[i].el, null);
}
}
// 移除整个Fragment
remove() {
if (!this.children.length) return;
const parent = this.children[0].el.parentNode;
for (let i = 0; i < this.children.length; i++) {
parent.removeChild(this.children[i].el);
}
}
// 移动整个Fragment
moveBefore(refNode) {
const parent = this.children[0].el.parentNode;
this.remove();
this.insert(parent, refNode);
}
}
Teleport:跨越DOM边界的传送
Teleport 的设计思想
Teleport(在Vue2中称为 Portal):允许将一段 DOM 内容"传送"到指定的目标位置:
// Teleport的VNode结构
const Teleport = {
__isTeleport: true,
// 处理逻辑
process(oldVNode, newVNode, container, anchor, internals) {
// 渲染逻辑
},
// 移除逻辑
remove(vnode, unmountChildren) {
// 移除逻辑
}
};
// Teleport的VNode
const teleportVNode = {
type: Teleport,
props: {
to: 'body', // 目标位置
disabled: false // 是否禁用传送
},
children: [ // 要传送的内容
h('div', '模态框内容')
],
shapeFlag: ShapeFlags.TELEPORT | ShapeFlags.ARRAY_CHILDREN,
target: null, // 目标容器(运行时设置)
targetAnchor: null // 目标容器的锚点
};
Teleport的工作流程
Teleport的核心实现
const TeleportImpl = {
__isTeleport: true,
/**
* 处理Teleport的渲染和更新
*/
process(oldVNode, newVNode, container, anchor, internals) {
const { patch, patchChildren, move } = internals;
if (oldVNode == null) {
// 首次挂载
const target = getTarget(newVNode.props.to);
const disabled = newVNode.props.disabled;
// 保存目标容器
newVNode.target = target;
newVNode.disabled = disabled;
// 确定挂载位置
const mountContainer = disabled ? container : target;
const mountAnchor = disabled ? anchor : null;
// 挂载子节点
patch(null, newVNode.children, mountContainer, mountAnchor);
// 记录第一个和最后一个子节点
newVNode.el = newVNode.children[0]?.el;
newVNode.anchor = newVNode.children[newVNode.children.length - 1]?.el;
} else {
// 更新
newVNode.target = oldVNode.target;
newVNode.disabled = oldVNode.disabled;
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
// 检查to属性是否变化
const targetChanged = newVNode.props.to !== oldVNode.props.to;
if (targetChanged) {
// 目标容器变化,需要移动所有子节点
const newTarget = getTarget(newVNode.props.to);
newVNode.target = newTarget;
// 从旧目标移除所有子节点
for (let i = 0; i < oldChildren.length; i++) {
const child = oldChildren[i];
if (child.el && child.el.parentNode) {
child.el.parentNode.removeChild(child.el);
}
}
// 挂载到新目标
for (let i = 0; i < newChildren.length; i++) {
patch(null, newChildren[i], newTarget, null);
}
} else {
// 检查disabled是否变化
const disabledChanged = newVNode.props.disabled !== oldVNode.props.disabled;
if (disabledChanged) {
newVNode.disabled = newVNode.props.disabled;
// 切换挂载位置
const from = oldVNode.disabled ? container : oldVNode.target;
const to = newVNode.disabled ? container : newVNode.target;
// 移动所有子节点
for (let i = 0; i < oldChildren.length; i++) {
const child = oldChildren[i];
if (child.el && child.el.parentNode === from) {
from.removeChild(child.el);
to.appendChild(child.el);
}
}
}
// 更新子节点内容
const container = newVNode.disabled ? container : newVNode.target;
patchChildren(oldVNode, newVNode, container);
}
}
},
/**
* 移除Teleport的子节点
*/
remove(vnode, unmountChildren) {
const { children, target } = vnode;
const container = vnode.disabled ? null : target;
if (container) {
// 从目标容器移除
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.el && child.el.parentNode === container) {
container.removeChild(child.el);
}
}
}
// 卸载子节点
unmountChildren(children);
}
};
获取目标容器
/**
* 解析to属性,获取目标DOM元素
*/
function getTarget(target) {
if (typeof target === 'string') {
// CSS选择器
const el = document.querySelector(target);
if (!el) {
console.warn(`目标容器 "${target}" 不存在`);
return document.body;
}
return el;
} else if (target instanceof HTMLElement) {
// 直接传入DOM元素
return target;
} else if (target?.$el) {
// Vue组件实例
return target.$el;
}
return document.body;
}
/**
* 解析to属性,支持动态值
*/
function resolveTarget(props, component) {
let target = props.to;
if (typeof target === 'function') {
target = target();
}
if (target?.__v_isRef) {
target = target.value;
}
if (target?.__v_isReactive) {
target = target.to;
}
return getTarget(target);
}
特殊节点在 Diff 中的处理
Fragment 在 Diff 中的特殊处理
由于 Fragment 没有自己的 DOM 元素,在 Diff 过程中需要特殊处理:
function patchKeyedChildren(oldChildren, newChildren, container) {
// 检查父节点是否为Fragment
const isFragmentParent = container.__v_isFragment;
let i = 0;
let oldEnd = oldChildren.length - 1;
let newEnd = newChildren.length - 1;
// 处理相同的前缀
while (i <= oldEnd && i <= newEnd) {
const oldVNode = oldChildren[i];
const newVNode = newChildren[i];
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode, container);
} else {
break;
}
i++;
}
// 处理相同的后缀
while (i <= oldEnd && i <= newEnd) {
const oldVNode = oldChildren[oldEnd];
const newVNode = newChildren[newEnd];
if (isSameVNodeType(oldVNode, newVNode)) {
patch(oldVNode, newVNode, container);
} else {
break;
}
oldEnd--;
newEnd--;
}
// 简单的增删情况
if (i > oldEnd) {
// 挂载剩余新节点
for (let j = i; j <= newEnd; j++) {
const newVNode = newChildren[j];
const anchor = newChildren[newEnd + 1]?.el;
// Fragment的子节点挂载不需要锚点
if (isFragmentParent) {
patch(null, newVNode, container, null);
} else {
patch(null, newVNode, container, anchor);
}
}
return;
}
if (i > newEnd) {
// 卸载剩余旧节点
for (let j = i; j <= oldEnd; j++) {
unmount(oldChildren[j]);
}
return;
}
// 核心diff(与普通节点相同,但移动操作使用不同的锚点)
const oldStart = i;
const newStart = i;
// 构建新节点索引表
const keyToNewIndexMap = new Map();
for (let j = newStart; j <= newEnd; j++) {
keyToNewIndexMap.set(newChildren[j].key, j);
}
// 构建位置数组
const toBePatched = newEnd - newStart + 1;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
for (let j = oldStart; j <= oldEnd; j++) {
const oldVNode = oldChildren[j];
const newIndex = keyToNewIndexMap.get(oldVNode.key);
if (newIndex === undefined) {
unmount(oldVNode);
} else {
newIndexToOldIndexMap[newIndex - newStart] = j + 1;
patch(oldVNode, newChildren[newIndex], container);
}
}
// 计算最长递增子序列
const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap);
// 移动节点(使用正确的锚点)
let lastIndex = increasingNewIndexSequence.length - 1;
for (let j = toBePatched - 1; j >= 0; j--) {
const newIndex = j + newStart;
const newVNode = newChildren[newIndex];
if (newIndexToOldIndexMap[j] === 0) {
// 新节点
patch(null, newVNode, container, null);
} else if (j !== increasingNewIndexSequence[lastIndex]) {
// 需要移动
const anchor = newChildren[newIndex + 1]?.el;
container.insertBefore(newVNode.el, anchor);
} else {
lastIndex--;
}
}
}
Teleport 在 Diff 中的处理
Teleport 的更新,需要考虑目标容器的变化:
function patchTeleport(oldVNode, newVNode, container, internals) {
const { patchChildren, move, unmount } = internals;
// 保存状态
newVNode.target = oldVNode.target;
newVNode.disabled = oldVNode.disabled;
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
// 检查to属性是否变化
const toChanged = oldProps.to !== newProps.to;
// 检查disabled是否变化
const disabledChanged = oldProps.disabled !== newProps.disabled;
if (toChanged) {
// 目标容器变化
const newTarget = getTarget(newProps.to);
newVNode.target = newTarget;
// 从旧目标移除所有子节点
const oldTarget = oldVNode.target;
if (oldTarget && !oldVNode.disabled) {
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === oldTarget) {
oldTarget.removeChild(child.el);
}
}
}
// 挂载到新目标
if (!newVNode.disabled) {
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, newTarget, null);
}
}
} else if (disabledChanged) {
// disabled状态变化
newVNode.disabled = newProps.disabled;
if (newVNode.disabled) {
// 从目标移到当前位置
const target = oldVNode.target;
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === target) {
target.removeChild(child.el);
}
}
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, container, null);
}
} else {
// 从当前位置移到目标
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === container) {
container.removeChild(child.el);
}
}
const target = newVNode.target;
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, target, null);
}
}
} else {
// 只有内容变化
const patchContainer = newVNode.disabled ? container : newVNode.target;
patchChildren(oldVNode, newVNode, patchContainer);
}
}
手写实现:支持 Fragment 的 Renderer
完整的 Renderer 实现
// 节点类型标识
const ShapeFlags = {
ELEMENT: 1,
COMPONENT: 1 << 1,
TEXT_CHILDREN: 1 << 2,
ARRAY_CHILDREN: 1 << 3,
FRAGMENT: 1 << 4,
TELEPORT: 1 << 5
};
// 特殊节点类型
const Fragment = Symbol('Fragment');
const Text = Symbol('Text');
const Comment = Symbol('Comment');
class Renderer {
constructor(options = {}) {
this.options = {
createElement: options.createElement || (tag => document.createElement(tag)),
createText: options.createText || (text => document.createTextNode(text)),
createComment: options.createComment || (text => document.createComment(text)),
insert: options.insert || ((child, parent, anchor) => parent.insertBefore(child, anchor)),
remove: options.remove || (child => child.parentNode?.removeChild(child)),
setText: options.setText || ((node, text) => node.nodeValue = text),
setElementText: options.setElementText || ((el, text) => el.textContent = text),
...options
};
}
/**
* 渲染入口
*/
render(vnode, container) {
if (vnode) {
this.patch(container._vnode || null, vnode, container);
container._vnode = vnode;
} else if (container._vnode) {
this.unmount(container._vnode);
container._vnode = null;
}
}
/**
* patch核心
*/
patch(oldVNode, newVNode, container, anchor = null) {
if (oldVNode === newVNode) return;
// 类型不同,直接替换
if (oldVNode && !this.isSameVNodeType(oldVNode, newVNode)) {
this.unmount(oldVNode);
oldVNode = null;
}
const { type, shapeFlag } = newVNode;
// 根据类型分发
if (type === Text) {
this.processText(oldVNode, newVNode, container, anchor);
} else if (type === Comment) {
this.processComment(oldVNode, newVNode, container, anchor);
} else if (type === Fragment) {
this.processFragment(oldVNode, newVNode, container, anchor);
} else if (type?.__isTeleport) {
this.processTeleport(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.ELEMENT) {
this.processElement(oldVNode, newVNode, container, anchor);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
this.processComponent(oldVNode, newVNode, container, anchor);
}
}
/**
* 处理Fragment
*/
processFragment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
// 首次挂载
this.mountFragment(newVNode, container, anchor);
} else {
// 更新
this.patchFragment(oldVNode, newVNode, container, anchor);
}
}
/**
* 挂载Fragment
*/
mountFragment(vnode, container, anchor) {
const { children, shapeFlag } = vnode;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 文本子节点
const textNode = this.options.createText(children);
vnode.el = textNode;
vnode.anchor = textNode;
this.options.insert(textNode, container, anchor);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 数组子节点
this.mountChildren(children, container, anchor);
// 记录范围
if (children.length > 0) {
vnode.el = children[0].el;
vnode.anchor = children[children.length - 1].el;
}
}
// 标记容器为Fragment父节点(用于后续锚点处理)
container.__isFragmentParent = true;
}
/**
* 更新Fragment
*/
patchFragment(oldVNode, newVNode, container, anchor) {
// Fragment本身没有DOM,直接更新子节点
this.patchChildren(oldVNode, newVNode, container);
// 更新范围
const newChildren = newVNode.children;
if (Array.isArray(newChildren) && newChildren.length > 0) {
newVNode.el = newChildren[0].el || oldVNode.el;
newVNode.anchor = newChildren[newChildren.length - 1].el || oldVNode.anchor;
}
}
/**
* 处理Teleport
*/
processTeleport(oldVNode, newVNode, container, anchor) {
const teleport = newVNode.type;
if (oldVNode == null) {
// 首次挂载
teleport.process(null, newVNode, container, anchor, {
patch: this.patch.bind(this),
patchChildren: this.patchChildren.bind(this),
mountChildren: this.mountChildren.bind(this),
unmount: this.unmount.bind(this),
move: this.move.bind(this)
});
} else {
// 更新
teleport.process(oldVNode, newVNode, container, anchor, {
patch: this.patch.bind(this),
patchChildren: this.patchChildren.bind(this),
mountChildren: this.mountChildren.bind(this),
unmount: this.unmount.bind(this),
move: this.move.bind(this)
});
}
}
/**
* 处理元素节点
*/
processElement(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
this.mountElement(newVNode, container, anchor);
} else {
this.patchElement(oldVNode, newVNode);
}
}
/**
* 挂载元素
*/
mountElement(vnode, container, anchor) {
const el = this.options.createElement(vnode.type);
vnode.el = el;
// 处理属性
if (vnode.props) {
for (const key in vnode.props) {
this.patchProp(el, key, null, vnode.props[key]);
}
}
// 处理子节点
const { shapeFlag, children } = vnode;
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(el, children);
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.mountChildren(children, el);
}
this.options.insert(el, container, anchor);
}
/**
* 挂载子节点数组
*/
mountChildren(children, container, anchor = null) {
for (let i = 0; i < children.length; i++) {
this.patch(null, children[i], container, anchor);
}
}
/**
* 更新子节点(核心diff)
*/
patchChildren(oldVNode, newVNode, container) {
const oldChildren = oldVNode.children;
const newChildren = newVNode.children;
const oldShapeFlag = oldVNode.shapeFlag;
const newShapeFlag = newVNode.shapeFlag;
// 判断父节点是否为Fragment
const isFragmentParent = container.__isFragmentParent;
// 新子节点是文本
if (newShapeFlag & ShapeFlags.TEXT_CHILDREN) {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.unmountChildren(oldChildren);
}
if (oldChildren !== newChildren) {
this.options.setElementText(container, newChildren);
}
}
// 新子节点是数组
else if (newShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(container, '');
this.mountChildren(newChildren, container);
} else if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.patchKeyedChildren(oldChildren, newChildren, container, isFragmentParent);
}
}
// 新子节点为空
else {
if (oldShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
this.unmountChildren(oldChildren);
} else if (oldShapeFlag & ShapeFlags.TEXT_CHILDREN) {
this.options.setElementText(container, '');
}
}
}
/**
* 带key的子节点diff
*/
patchKeyedChildren(oldChildren, newChildren, container, isFragmentParent = false) {
let i = 0;
let oldEnd = oldChildren.length - 1;
let newEnd = newChildren.length - 1;
// 处理相同前缀
while (i <= oldEnd && i <= newEnd && this.isSameVNodeType(oldChildren[i], newChildren[i])) {
this.patch(oldChildren[i], newChildren[i], container);
i++;
}
// 处理相同后缀
while (i <= oldEnd && i <= newEnd && this.isSameVNodeType(oldChildren[oldEnd], newChildren[newEnd])) {
this.patch(oldChildren[oldEnd], newChildren[newEnd], container);
oldEnd--;
newEnd--;
}
// 旧节点遍历完,挂载剩余新节点
if (i > oldEnd) {
for (let j = i; j <= newEnd; j++) {
const newVNode = newChildren[j];
const anchor = newChildren[newEnd + 1]?.el;
this.patch(null, newVNode, container, isFragmentParent ? null : anchor);
}
return;
}
// 新节点遍历完,卸载剩余旧节点
if (i > newEnd) {
for (let j = i; j <= oldEnd; j++) {
this.unmount(oldChildren[j]);
}
return;
}
// 核心diff:处理未知序列
const oldStart = i;
const newStart = i;
// 建立新节点索引表
const keyToNewIndexMap = new Map();
for (let j = newStart; j <= newEnd; j++) {
keyToNewIndexMap.set(newChildren[j].key, j);
}
// 构建位置数组
const toBePatched = newEnd - newStart + 1;
const newIndexToOldIndexMap = new Array(toBePatched).fill(0);
for (let j = oldStart; j <= oldEnd; j++) {
const oldVNode = oldChildren[j];
const newIndex = keyToNewIndexMap.get(oldVNode.key);
if (newIndex === undefined) {
this.unmount(oldVNode);
} else {
newIndexToOldIndexMap[newIndex - newStart] = j + 1;
this.patch(oldVNode, newChildren[newIndex], container);
}
}
// 计算最长递增子序列
const increasingNewIndexSequence = this.getSequence(newIndexToOldIndexMap);
// 移动节点
let lastIndex = increasingNewIndexSequence.length - 1;
for (let j = toBePatched - 1; j >= 0; j--) {
const newIndex = j + newStart;
const newVNode = newChildren[newIndex];
if (newIndexToOldIndexMap[j] === 0) {
// 新节点,需要挂载
const anchor = newChildren[newIndex + 1]?.el;
this.patch(null, newVNode, container, isFragmentParent ? null : anchor);
} else if (j !== increasingNewIndexSequence[lastIndex]) {
// 需要移动
const anchor = newChildren[newIndex + 1]?.el;
this.options.insert(newVNode.el, container, anchor);
} else {
lastIndex--;
}
}
}
/**
* 处理文本节点
*/
processText(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
const textNode = this.options.createText(newVNode.children);
newVNode.el = textNode;
this.options.insert(textNode, container, anchor);
} else {
const el = (newVNode.el = oldVNode.el);
if (newVNode.children !== oldVNode.children) {
this.options.setText(el, newVNode.children);
}
}
}
/**
* 处理注释节点
*/
processComment(oldVNode, newVNode, container, anchor) {
if (oldVNode == null) {
const commentNode = this.options.createComment(newVNode.children);
newVNode.el = commentNode;
this.options.insert(commentNode, container, anchor);
} else {
newVNode.el = oldVNode.el;
}
}
/**
* 更新属性
*/
patchProp(el, key, prev, next) {
if (key.startsWith('on')) {
const eventName = key.slice(2).toLowerCase();
if (prev) {
el.removeEventListener(eventName, prev);
}
if (next) {
el.addEventListener(eventName, next);
}
} else if (key === 'class') {
el.className = next || '';
} else if (key === 'style') {
if (typeof next === 'string') {
el.style.cssText = next;
} else if (next) {
Object.assign(el.style, next);
}
} else if (next == null) {
el.removeAttribute(key);
} else {
el.setAttribute(key, next);
}
}
/**
* 判断两个节点是否相同
*/
isSameVNodeType(n1, n2) {
return n1.type === n2.type && n1.key === n2.key;
}
/**
* 计算最长递增子序列
*/
getSequence(arr) {
const len = arr.length;
const result = [0];
const p = new Array(len).fill(0);
for (let i = 0; i < len; i++) {
const val = arr[i];
if (val === 0) continue;
let low = 0;
let high = result.length - 1;
while (low < high) {
const mid = (low + high) >> 1;
if (arr[result[mid]] < val) {
low = mid + 1;
} else {
high = mid;
}
}
if (arr[result[low]] < val) {
result.push(i);
p[i] = result[low];
} else {
result[low] = i;
p[i] = result[low - 1];
}
}
let u = result.length;
let v = result[u - 1];
while (u-- > 0) {
result[u] = v;
v = p[v];
}
return result;
}
/**
* 卸载节点
*/
unmount(vnode) {
const { type, shapeFlag, children } = vnode;
if (type === Fragment) {
// 卸载Fragment的所有子节点
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
this.unmount(children[i]);
}
}
} else if (type?.__isTeleport) {
// 卸载Teleport
type.remove(vnode, this.unmount.bind(this));
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 卸载组件
this.unmountComponent(vnode);
} else if (vnode.el) {
this.options.remove(vnode.el);
}
}
/**
* 卸载子节点数组
*/
unmountChildren(children) {
for (let i = 0; i < children.length; i++) {
this.unmount(children[i]);
}
}
/**
* 移动节点
*/
move(vnode, container, anchor) {
this.options.insert(vnode.el, container, anchor);
}
}
Teleport的实现
// Teleport组件定义
const Teleport = {
__isTeleport: true,
process(oldVNode, newVNode, container, anchor, internals) {
const { patch, patchChildren, move, unmount } = internals;
if (oldVNode == null) {
// 首次挂载
const target = this.resolveTarget(newVNode.props);
const disabled = newVNode.props.disabled || false;
newVNode.target = target;
newVNode.disabled = disabled;
const mountContainer = disabled ? container : target;
const mountAnchor = disabled ? anchor : null;
// 挂载子节点
if (Array.isArray(newVNode.children)) {
for (let i = 0; i < newVNode.children.length; i++) {
patch(null, newVNode.children[i], mountContainer, mountAnchor);
}
}
// 记录范围
newVNode.el = newVNode.children[0]?.el;
newVNode.anchor = newVNode.children[newVNode.children.length - 1]?.el;
} else {
// 更新
const oldProps = oldVNode.props || {};
const newProps = newVNode.props || {};
newVNode.target = oldVNode.target;
newVNode.disabled = oldVNode.disabled;
// 检查to是否变化
if (oldProps.to !== newProps.to) {
const newTarget = this.resolveTarget(newProps);
newVNode.target = newTarget;
// 从旧目标移除
if (!oldVNode.disabled) {
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === oldVNode.target) {
oldVNode.target.removeChild(child.el);
}
}
}
// 挂载到新目标
if (!newVNode.disabled) {
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, newVNode.target, null);
}
}
}
// 检查disabled是否变化
if (oldProps.disabled !== newProps.disabled) {
newVNode.disabled = newProps.disabled;
if (newVNode.disabled) {
// 从目标移到当前位置
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === oldVNode.target) {
oldVNode.target.removeChild(child.el);
}
}
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, container, anchor);
}
} else {
// 从当前位置移到目标
for (let i = 0; i < oldVNode.children.length; i++) {
const child = oldVNode.children[i];
if (child.el && child.el.parentNode === container) {
container.removeChild(child.el);
}
}
for (let i = 0; i < newVNode.children.length; i++) {
const child = newVNode.children[i];
patch(null, child, newVNode.target, null);
}
}
}
// 更新内容
const patchContainer = newVNode.disabled ? container : newVNode.target;
patchChildren(oldVNode, newVNode, patchContainer);
}
},
remove(vnode, unmountChildren) {
const { children, target, disabled } = vnode;
if (!disabled && target) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (child.el && child.el.parentNode === target) {
target.removeChild(child.el);
}
}
}
unmountChildren(children);
},
resolveTarget(props) {
const to = props.to;
if (typeof to === 'string') {
return document.querySelector(to) || document.body;
}
if (to instanceof HTMLElement) {
return to;
}
return document.body;
}
};
结语
Fragment 和 Teleport 是 Vue3 中两个重要的新特性,它们打破了传统 DOM 树的限制,让我们能以更灵活的方式组织组件。理解它们的实现原理,不仅能帮助我们更好地使用这些特性,也能在遇到复杂场景时找到合适的解决方案。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!