
💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》
1、前言
Teleport组件是 Vue3 中内置的组件,可以将指定的内容渲染到特定的容器中,且不受 DOM 层级的限制,这在很多场景下非常有用,比如模态框、通知、弹出菜单等。例如,element-plus组件库中的 el-dialog组件底层就是使用 Teleport组件实现的,如下图所示:

2、Teleport 组件源码实现
2.1 实现原理
假如有以下模版内容:
            
            
              html
              
              
            
          
          <template>
  <button @click="open = true">Open Modal</button>
  <Teleport to="body">
    <div v-if="open" class="modal">
      <p>Hello from the modal!</p>
      <button @click="open = false">Close</button>
    </div>
  </Teleport>
</template>
        借助Vue SFC Playground平台,它会被编译成如下内容:
            
            
              javascript
              
              
            
          
          return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = $event => (_isRef(open) ? open.value = true : open = true))
    }, "Open Modal"),
    (_openBlock(), _createBlock(_Teleport, { to: "body" }, [
      (_unref(open))
        ? (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("button", {
              onClick: _cache[1] || (_cache[1] = $event => (_isRef(open) ? open.value = false : open = false))
            }, "Close")
          ]))
        : _createCommentVNode("v-if", true)
    ]))
  ], 64 /* STABLE_FRAGMENT */))
}
        可以看到,对于 Teleport标签,它是直接创建了 Teleport内置组件,我们来看它的实现:
            
            
              javascript
              
              
            
          
          export const TeleportImpl = {
  name: 'Teleport',
  // Telport标识符
  __isTeleport: true,
  /**
   * 客户端渲染函数,优点:
   * 1.可以避免渲染器逻辑代码膨胀
   * 2.可以使用Tree-shaking删除代码,使得代码体积变小
   */
  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean,
    internals: RendererInternals
  ) {
    // 创建逻辑
    if (n1 == null) {
    } else {
      // 更新逻辑
    }
  },
  // 删除逻辑
  remove(
    vnode: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    optimized: boolean,
    { um: unmount, o: { remove: hostRemove } }: RendererInternals,
    doRemove: boolean
  ) {
  },
  // 移动Teleport
  move: moveTeleport,
  // 服务端渲染Teleport
  hydrate: hydrateTeleport
}
        Teleport 组件其实就是一个对象,对外提供了 process、remove、move方法,它们的作用分别是:
- process :组件的创建和更新逻辑。将该逻辑剥离的原因有:1、避免渲染器的代码过多,2、可以利用 
Tree-shaking删除Teleport相关的代码,减小代码体积; - remove:组件的删除逻辑;
 - move:组件的移动逻辑。
 
接下来我们就从这三个函数的实现流程来分析 Teleport 组件的实现原理。
2.2 创建流程
在 patch函数中对 process函数的调用如下:
            
            
              javascript
              
              
            
          
          // packages/runtime-core/src/renderer.ts
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 省略部分代码
    const { type, ref, shapeFlag } = n2
    switch (type) {
      // 省略部分代码
      default:
        // 省略部分代码
        // shapeFlag 的类型为 TELEPORT,则它是 Teleport 组件
        // 调用 Teleport 组件选项中的 process 函数将控制权交接出去
        // 传递给 process 函数的第五个参数是渲染器的一些内部方法
        else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
        // 省略部分代码
    }
    // 省略部分代码
  }
        当判断 vnode的类型是 Teleport时,直接调用组件选项中定义的 process 方法将渲染控制权完全交接出去 ,这样就实现了渲染逻辑的分离。接下来我们来看看 Teleport中的 process方法实现逻辑:
以上代码处理流程如下:
- 
- 
创建一个注释节点或文本节点作为占位符
placeholder,并将其赋值给n2.el。如果是开发环境 (__DEV__为true),则创建一个注释节点 "teleport start",否则创建一个空文本节点。 - 
创建一个注释节点或文本节点作为锚点
mainAnchor,并将其赋值给n2.anchor。如果是开发环境,创建一个注释节点 "teleport end",否则创建一个空文本节点。 - 
使用
insert函数将placeholder和mainAnchor插入到 DOM 中的container元素里,位置是anchor指定的位置。第 1 步作用是:非生产环境往 Teleport 组件原本的位置插入注释节点,在生产环境插入空白文本节点。
typescriptconst placeholder = (n2.el = __DEV__ ? createComment('teleport start') : createText('')) const mainAnchor = (n2.anchor = __DEV__ ? createComment('teleport end') : createText('')) insert(placeholder, container, anchor) insert(mainAnchor, container, anchor) 
 - 
 - 
- 
通过
resolveTarget函数解析Teleport的目标元素,并将其赋值给n2.target。同时创建一个空文本节点targetAnchor并赋值给n2.targetAnchor。 - 
如果找到了有效的
target,则将targetAnchor插入到target中。如果在 SVG 上下文中,还会设置isSVG标志。 - 
如果在开发环境中,且
target无效且disabled未设置为true,则会发出警告。第 2 步的作用是:拿到目标节点 target,并在目标节点位置创建了空的文本节点作为锚点。
typescript// 获取容器,即挂载点 const target = (n2.target = resolveTarget(n2.props, querySelector)) const targetAnchor = (n2.targetAnchor = createText('')) // 如果挂载点存在 if (target) { insert(targetAnchor, target) // #2652 we could be teleporting from a non-SVG tree into an SVG tree isSVG = isSVG || isTargetSVG(target) } else if (__DEV__ && !disabled) { warn('Invalid Teleport target on mount:', target, `(${typeof target})`) } 
 - 
 - 
- 
定义
mount函数,它接受container和anchor作为参数。如果Teleport的子节点是数组类型的子节点,则它会调用mountChildren函数来渲染这些子节点。第 3 步的作用是:定义渲染子节点的方法。
typescript// 将n2.children渲染到指定挂载点 const mount = (container: RendererElement, anchor: RendererNode) => { // Teleport *always* has Array children. This is enforced in both the // compiler and vnode children normalization. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 调用渲染器内部的 mountChildren 方法渲染 Teleport 组件的插槽内容 mountChildren( children as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } } 
 - 
 - 
- 
如果
disabled为true,则在当前的container和mainAnchor上调用mount函数; - 
如果
disabled为false且存在有效的target,则在target和targetAnchor上调用mount函数;第 4 步的作用是:根据 disabled 属性,实现真正的渲染子节点。
typescript// 如果 Teleport 组件的 disabled 为 true,说明禁用了 <teleport> 的功能,Teleport 只会在 container 中渲染 if (disabled) { mount(container, mainAnchor) } else if (target) { // 如果没有禁用 <teleport> 的功能,并且存在挂载点,则将其插槽内容渲染到target容中 mount(target, targetAnchor) } 
 - 
 
整体来看,这段代码负责创建 Teleport 组件的占位符和锚点,将它们插入到 DOM 中,并根据 disabled 属性决定是在原位置渲染子组件还是在 Teleport 的目标位置渲染子组件。
2.3 更新流程
            
            
              typescript
              
              
            
          
          // update content
  n2.el = n1.el
  const mainAnchor = (n2.anchor = n1.anchor)!
  // 挂载点
  const target = (n2.target = n1.target)!
  // 锚点
  const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  // 判断Telport组件是否禁用了
  const wasDisabled = isTeleportDisabled(n1.props)
  // 如果禁用了,那么挂载点就是周围父组件,否则就是to指定的目标挂载点
  const currentContainer = wasDisabled ? container : target
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  // 目标挂载点是否是SVG标签元素
  isSVG = isSVG || isTargetSVG(target)
  if (dynamicChildren) {
    // fast path when the teleport happens to be a block root
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      currentContainer,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds
    )
    // even in block tree mode we need to make sure all root-level nodes
    // in the teleport inherit previous DOM references so that they can
    // be moved in future patches.
    traverseStaticChildren(n1, n2, true)
  } else if (!optimized) {
    patchChildren(
      n1,
      n2,
      currentContainer,
      currentAnchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      false
    )
  }
  if (disabled) {
    if (!wasDisabled) {
      // enabled -> disabled
      // move into main container
      moveTeleport(
        n2,
        container,
        mainAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    } else {
      // #7835
      // When `teleport` is disabled, `to` may change, making it always old,
      // to ensure the correct `to` when enabled
      if (n2.props && n1.props && n2.props.to !== n1.props.to) {
        n2.props.to = n1.props.to
      }
    }
  } else {
    // target changed
    if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
      const nextTarget = (n2.target = resolveTarget(
        n2.props,
        querySelector
      ))
      if (nextTarget) {
        moveTeleport(
          n2,
          nextTarget,
          null,
          internals,
          TeleportMoveTypes.TARGET_CHANGE
        )
      } else if (__DEV__) {
        warn(
          'Invalid Teleport target on update:',
          target,
          `(${typeof target})`
        )
      }
    } else if (wasDisabled) {
      // disabled -> enabled
      // move into teleport target
      moveTeleport(
        n2,
        target,
        targetAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    }
  }
}
        Teleport组件更新主要做了以下几件事情:更新子节点、处理 disabled属性变化、处理 to属性变化的情况。更新流程的处理步骤拆解如下:
- 更新子节点:可以分为优化更新和普通的全量对比更新两种情况;
 - 处理 
disabled属性变化的情况:如果满足新节点disabled为true,且旧节点的disabled为false的话,说明我们需要把Teleport的子节点从目标元素内部移回到主视图内部了,此时执行相应的处理逻辑; - 处理 
to属性变化的情况:如果新节点的disabed为true,那么先通过to属性是否改变来判断目标元素target有没有变化,如果有变化,则把Teleport的子节点移动到新的target内部;如果目标元素没变化,则判断旧节点的disabled是否为true,如果是则把Teleport的子节点从主视图内部移动到目标元素内部了。 
2.3 卸载流程
当组件卸载的时候会执行 umount方法,它的内部会判断卸载的组件是否是 Teleport,如果是则会执行 Teleport组件的 remove方法,如下代码所示:
            
            
              typescript
              
              
            
          
           if (shapeFlag & ShapeFlags.TELEPORT) {
  ;(vnode.type as typeof TeleportImpl).remove(
    vnode,
    parentComponent,
    parentSuspense,
    optimized,
    internals,
    doRemove
  )
}
        我们来看看 Teleport组件 remove方法的实现:
            
            
              typescript
              
              
            
          
          // 移除Telport
remove(
  vnode: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  optimized: boolean,
  { um: unmount, o: { remove: hostRemove } }: RendererInternals,
  doRemove: boolean
) {
  const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
  if (target) {
    hostRemove(targetAnchor!)
  }
  // an unmounted teleport should always unmount its children whether it's disabled or not
  doRemove && hostRemove(anchor!)
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    const shouldRemove = doRemove || !isTeleportDisabled(props)
    for (let i = 0; i < (children as VNode[]).length; i++) {
      const child = (children as VNode[])[i]
      unmount(
        child,
        parentComponent,
        parentSuspense,
        shouldRemove,
        !!child.dynamicChildren
      )
    }
  }
},
        卸载流程中,首先调用 hostRemove移除主视图渲染的锚点"teleport start"注释节点,然后再去遍历 Teleport 的子节点调用 remove方法移除子节点。
3、总结
本篇详细解析了 Teleport组件的使用场景,剖析了创建流程、更新流程以及卸载流程。总体流程如下: