Teleport 用来将组件内容渲染到当前组件树之外的 DOM 容器中。

2025-11-23 16:04:26

Teleport 用来将组件内容渲染到当前组件树之外的 DOM 容器中。

常用于模态框、弹出层等场景:组件逻辑仍然属于当前组件,但真实 DOM 可以挂载到其他位置。

<div id="app"></div>
<div id="modal"></div>
import { createApp, h, Teleport } from '../dist/vue.esm.js'

const App = {
  render() {
    return h('div', [
      h('p', 'app content'),
      h(
        Teleport,
        { to: '#modal' },
        [
          h('div', 'modal content')
        ]
      )
    ])
  }
}

createApp(App).mount('#app')

modal content 不会插入到 #app 内部,而是会插入到 #modal 中。

渲染流程

实现主要分为四步:

  1. 创建 vnode 时识别 Teleport,并设置 ShapeFlags.TELEPORT
  2. patch 过程中将 Teleport 分发给它自己的 process 方法
  3. 挂载或更新时,根据 to 和 disabled 决定子节点的容器
  4. 卸载时,直接卸载 Teleport 的子节点

实现

1. 识别为 Teleport

// packages/runtime-core/src/components/Teleport.ts
export const Teleport = {
  name: 'Teleport',
  __isTeleport: true,
  props: {
    to: { type: String },
    disabled: { type: Boolean }
  }
}

__isTeleport: 识别 Teleport 标记。

Teleport 核心属性:

  • to:目标容器的选择器
  • disabled:是否禁用。禁用时,子节点会渲染到当前容器

在 createVNode 中,根据 type 设置对应的 shapeFlag:

export function createVNode(type, props?, children = null) {
  let shapeFlag = 0

  if (isString(type)) {
    shapeFlag = ShapeFlags.ELEMENT
  }
  else if (isTeleport(type)) {
    shapeFlag = ShapeFlags.TELEPORT
  }
  else if (isObject(type)) {
    shapeFlag = ShapeFlags.STATEFUL_COMPONENT
  }
  else if (isFunction(type)) {
    shapeFlag = ShapeFlags.FUNCTIONAL_COMPONENT
  }

  const vnode = {
    type,
    props,
    children,
    shapeFlag
    // ...
  }

  return vnode
}

当 type 是 Teleport 时,vnode 的 shapeFlag 就是 ShapeFlags.TELEPORT。

2. 分发 Teleport

function patch(n1, n2, container, anchor = null, parentComponent = null) {
  // ...
  const { shapeFlag, type } = n2

  switch (type) {
    case Text:
      processText(n1, n2, container, anchor)
      break

    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        processElement(n1, n2, container, anchor, parentComponent)
      }
      else if (shapeFlag & ShapeFlags.COMPONENT) {
        processComponent(n1, n2, container, anchor, parentComponent)
      }
      else if (shapeFlag & ShapeFlags.TELEPORT) {
        type.process(n1, n2, container, anchor, parentComponent, {
          mountChildren,
          patchChildren,
          options
        })
      }
  }
}

Teleport 直接调用自身的 process 方法,将 mountChildren、patchChildren 和平台相关的 options 传递。

这样 Teleport 只负责决定渲染位置,具体的节点挂载和更新仍然复用渲染器已有的逻辑。

3. 挂载和更新 Teleport

export const isTeleport = type => type.__isTeleport

export const Teleport = {
  name: 'Teleport',
  __isTeleport: true,
  props: {
    to: {
      type: String
    },
    disabled: {
      type: Boolean
    }
  },

  process(n1, n2, container, anchor, parentComponent, internals) {
    const {
      mountChildren,
      patchChildren,
      options: { querySelector, insert }
    } = internals

    const { to, disabled } = n2.props

    if (n1 == null) {
      const target = disabled ? container : querySelector(to)

      if (target) {
        n2.target = target
        mountChildren(target, n2.children, parentComponent)
      }
      return
    }

    // 先在旧容器中更新子节点
    patchChildren(n1, n2, n1.target, parentComponent)
    n2.target = n1.target

    const prevProps = n1.props || {}

    // to 或 disabled 发生变化时,移动子节点
    if (prevProps.to !== to || prevProps.disabled !== disabled) {
      const target = disabled ? container : querySelector(to)

      if (target) {
        for (const child of n2.children) {
          insert(child.el, target)
        }

        n2.target = target
      }
    }
  }
}

挂载阶段的处理比较直接:

  • disabled 为 true 时,使用当前的 container
  • disabled 为 false 时,通过 querySelector(to) 查找目标容器
  • 找到目标容器后,记录 n2.target,并调用 mountChildren 挂载子节点

更新阶段需要先处理子节点本身的变化,再处理目标容器的变化:

  • 调用 patchChildren,在旧容器中更新子节点
  • 对比新旧 to 和 disabled
  • 如果目标容器发生变化,就使用 insert 将子节点移动到新的容器
  • 更新 n2.target,保存当前实际使用的容器

4. 卸载 Teleport

Teleport 没有对应的真实 DOM 元素,真正插入页面的是它的子节点。

function unmount(vnode) {
  const { shapeFlag, children, ref } = vnode

  if (shapeFlag & ShapeFlags.COMPONENT) {
    unmountComponent(vnode.component)
  }
  else if (shapeFlag & ShapeFlags.TELEPORT) {
    unmountChildren(children)
    return
  }
  else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    unmountChildren(children)
  }

  hostRemove(vnode.el)

  if (ref != null) {
    setRef(ref, null)
  }
}

简单处理一下,只卸载子节点,然后直接 return。

如果继续执行 hostRemove(vnode.el),就会尝试删除 Teleport 自身的 DOM 节点。但 Teleport 没有对应的真实元素,实际需要删除的是已经挂载到目标容器中的子节点。