Fragment 与 Portal 的特殊处理

0 阅读11分钟

在前面的文章中,我们深入探讨了 Diff 算法和组件渲染的完整过程。今天,我们将聚焦于 Vue3 中两个特殊的节点类型——FragmentPortal/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 节点,这是通过 elanchor 实现的:

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的工作流程

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;
  }
};

结语

FragmentTeleport 是 Vue3 中两个重要的新特性,它们打破了传统 DOM 树的限制,让我们能以更灵活的方式组织组件。理解它们的实现原理,不仅能帮助我们更好地使用这些特性,也能在遇到复杂场景时找到合适的解决方案。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!