相关代码位置如下:

packages/runtime-core/src/components/Teleport.ts

packages/runtime-core/src/renderer.ts

Teleport 组件使用起来非常简单,套在想要在别处渲染的组件或者 DOM 节点的外部,然后通过 to 这个 prop 去指定渲染到的位置,to 可以是一个 DOM 选择器字符串,也可以是一个 DOM 节点。

// Dialog.vue
<script setup>
import { ref } from 'vue'

const visible = ref(false)

const show = () => {
  visible.value = true
}
</script>

<template>
  <div v-show="visible" class="dialog">
    <div class="dialog-body">
      <p>I'm a dialog!</p>
      <button @click="visible = false">Close</button>
    </div>
  </div>
</template>

<style lang="css" scoped>
.dialog {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  background-color: rgba(0, 0, 0, .5);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
}

.dialog .dialog-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: white;
  width: 300px;
  height: 300px;
  padding: 5px;
}
</style>
<script setup>
import { ref } from 'vue'
import Dialog from './components/Dialog.vue'

const dialog = ref(null)

const showDialog = () => {
  dialog.value?.show()
}
</script>
<template>
  <button @click="showDialog">Show dialog</button>
  <Dialog ref="dialog"></Dialog>
</template>

因为 Dialog 组件使用的是 position:absolute 绝对定位的方式,如果它的父级 DOM 有 position 不为 static 的布局方式,那么 Dialog 的定位就受到了影响,不能按预期渲染了。

所以一种好的解决方案是把 Dialog 组件渲染的这部分 DOM 挂载到 body 下面,这样就不会受到父级样式的影响了。

这也是 Teleport 组件要解决的问题。将指定内容渲染到特定容器中(跨 DOM 层级的渲染),而不受 DOM 层级的限制。

<script setup>
import { ref } from 'vue'
import Dialog from './components/Dialog.vue'

const dialog = ref(null)

const showDialog = () => {
  dialog.value?.show()
}
</script>
<template>
  <button @click="showDialog">Show dialog</button>
  <!-- 新增 Teleport -->
  <teleport to="body">
    <Dialog ref="dialog"></Dialog>
  </teleport>
</template>

Teleport 实现原理

<teleport to="body">
  <Dialog ref="dialog"></Dialog>
</teleport>

模版编译之后的结果:Teleport Explorer

import { resolveComponent as _resolveComponent, createVNode as _createVNode, Teleport as _Teleport, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  const _component_Dialog = _resolveComponent("Dialog")

  return (_openBlock(), _createBlock(_Teleport, { to: "body" }, [
    _createVNode(_component_Dialog, { ref: "dialog" }, null, 512 /* NEED_PATCH */)
  ]))
}

源码实现如下

export const TeleportImpl = {
  __isTeleport: true,
  process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals) {
	  if (n1 == null) {
	    // 创建逻辑
	  } else {
	    // 更新逻辑
	  }
	},
  remove(
    vnode,
    parentComponent,
    parentSuspense,
    optimized,
    { um: unmount, o: { remove: hostRemove } }: RendererInternals,
    doRemove: Boolean
  ) {
    // 删除逻辑
  },

  move: moveTeleport,
  hydrate: hydrateTeleport,
}

Teleport 组件的实现就是一个对象,对外提供了几个方法。其中 process 方法负责组件的创建和更新逻辑,remove 方法负责组件的删除逻辑。

创建

Teleport 组件创建部分主要分为三个步骤:

  1. 在主视图里插入注释节点或者空白文本节点

    Untitled

  2. 获取目标元素节点

  3. 往目标元素插入 Teleport 组件的子节点

const 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
) => {
  const {
    mc: mountChildren,
    pc: patchChildren,
    pbc: patchBlockChildren,
    o: { insert, querySelector, createText, createComment }
  } = internals

  // 是否禁用 Teleport
  const disabled = isTeleportDisabled(n2.props)
  let { shapeFlag, children, dynamicChildren } = n2

  // #3302
  // HMR updated, force full diff
  if (__DEV__ && isHmrUpdating) {
    optimized = false
    dynamicChildren = null
  }

  if (n1 == null) {
    // 在主视图里插入注释节点或者空白文本节点
    const 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)
    // 获取目标元素的 DOM 节点
    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})`)
    }

    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) {
        // 挂载子节点
        // (遍历 Teleport 组件的 children 属性,并逐一调用 patch 函数完成子节点的挂载)
        mountChildren(
          children as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      }
    }

    if (disabled) {
      // disabled 则在原来的位置挂载
      mount(container, mainAnchor)
    } else if (target) {
      // 挂载在 target 元素的位置
      mount(target, targetAnchor)
    }
  }
}

更新

当组件发生更新的时候,仍然会执行 patch 逻辑走到 Teleport 的 process 方法,去处理 Teleport 组件的更新

const 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
) => {
  const {
    mc: mountChildren,
    pc: patchChildren,
    pbc: patchBlockChildren,
    o: { insert, querySelector, createText, createComment },
  } = internals

  // 是否禁用 Teleport
  const disabled = isTeleportDisabled(n2.props)
  let { shapeFlag, children, dynamicChildren } = n2

  // #3302
  // HMR updated, force full diff
  if (__DEV__ && isHmrUpdating) {
    optimized = false
    dynamicChildren = null
  }

  if (n1 == null) {
    // ---------------------------
    // 创建逻辑
    // ---------------------------
  } else {
    // update content
    n2.el = n1.el
    const mainAnchor = (n2.anchor = n1.anchor)!
    const target = (n2.target = n1.target)!
    const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
    // 之前是不是 disabled 状态
    const wasDisabled = isTeleportDisabled(n1.props)
    const currentContainer = wasDisabled ? container : target
    const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
    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
        // 把子节点从目标元素移动回主容器内
        moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE
        )
      }
    } else {
      // 目标容器改变
      // (to 属性值修改)
      // 新旧 to 属性值不同,则需要对内容进行移动
      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
        // 移动到目标元素位置
        moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
      }
    }
  }
}

Teleport 组件更新:

移除