• Contents

相关代码位置如下:

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

packages/runtime-core/src/renderer.ts

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

  1. // Dialog.vue
  2. <script setup>
  3. import { ref } from 'vue'
  4. const visible = ref(false)
  5. const show = () => {
  6. visible.value = true
  7. }
  8. </script>
  9. <template>
  10. <div v-show="visible" class="dialog">
  11. <div class="dialog-body">
  12. <p>I'm a dialog!</p>
  13. <button @click="visible = false">Close</button>
  14. </div>
  15. </div>
  16. </template>
  17. <style lang="css" scoped>
  18. .dialog {
  19. position: absolute;
  20. top: 0;
  21. right: 0;
  22. bottom: 0;
  23. left: 0;
  24. background-color: rgba(0, 0, 0, .5);
  25. display: flex;
  26. flex-direction: column;
  27. align-items: center;
  28. justify-content: center;
  29. }
  30. .dialog .dialog-body {
  31. display: flex;
  32. flex-direction: column;
  33. align-items: center;
  34. justify-content: center;
  35. background-color: white;
  36. width: 300px;
  37. height: 300px;
  38. padding: 5px;
  39. }
  40. </style>
  1. <script setup>
  2. import { ref } from 'vue'
  3. import Dialog from './components/Dialog.vue'
  4. const dialog = ref(null)
  5. const showDialog = () => {
  6. dialog.value?.show()
  7. }
  8. </script>
  9. <template>
  10. <button @click="showDialog">Show dialog</button>
  11. <Dialog ref="dialog"></Dialog>
  12. </template>

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

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

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

  1. <script setup>
  2. import { ref } from 'vue'
  3. import Dialog from './components/Dialog.vue'
  4. const dialog = ref(null)
  5. const showDialog = () => {
  6. dialog.value?.show()
  7. }
  8. </script>
  9. <template>
  10. <button @click="showDialog">Show dialog</button>
  11. <!-- 新增 Teleport -->
  12. <teleport to="body">
  13. <Dialog ref="dialog"></Dialog>
  14. </teleport>
  15. </template>

Teleport 实现原理

  1. <teleport to="body">
  2. <Dialog ref="dialog"></Dialog>
  3. </teleport>

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

  1. import { resolveComponent as _resolveComponent, createVNode as _createVNode, Teleport as _Teleport, openBlock as _openBlock, createBlock as _createBlock } from "vue"
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. const _component_Dialog = _resolveComponent("Dialog")
  4. return (_openBlock(), _createBlock(_Teleport, { to: "body" }, [
  5. _createVNode(_component_Dialog, { ref: "dialog" }, null, 512 /* NEED_PATCH */)
  6. ]))
  7. }

源码实现如下

  1. export const TeleportImpl = {
  2. __isTeleport: true,
  3. process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals) {
  4. if (n1 == null) {
  5. // 创建逻辑
  6. } else {
  7. // 更新逻辑
  8. }
  9. },
  10. remove(
  11. vnode,
  12. parentComponent,
  13. parentSuspense,
  14. optimized,
  15. { um: unmount, o: { remove: hostRemove } }: RendererInternals,
  16. doRemove: Boolean
  17. ) {
  18. // 删除逻辑
  19. },
  20. move: moveTeleport,
  21. hydrate: hydrateTeleport,
  22. }

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

创建

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

  1. 在主视图里插入注释节点或者空白文本节点
    image.png
  2. 获取目标元素节点
  3. 往目标元素插入 Teleport 组件的子节点
  1. const process = (
  2. n1: TeleportVNode | null,
  3. n2: TeleportVNode,
  4. container: RendererElement,
  5. anchor: RendererNode | null,
  6. parentComponent: ComponentInternalInstance | null,
  7. parentSuspense: SuspenseBoundary | null,
  8. isSVG: boolean,
  9. slotScopeIds: string[] | null,
  10. optimized: boolean,
  11. internals: RendererInternals
  12. ) => {
  13. const {
  14. mc: mountChildren,
  15. pc: patchChildren,
  16. pbc: patchBlockChildren,
  17. o: { insert, querySelector, createText, createComment }
  18. } = internals
  19. // 是否禁用 Teleport
  20. const disabled = isTeleportDisabled(n2.props)
  21. let { shapeFlag, children, dynamicChildren } = n2
  22. // #3302
  23. // HMR updated, force full diff
  24. if (__DEV__ && isHmrUpdating) {
  25. optimized = false
  26. dynamicChildren = null
  27. }
  28. if (n1 == null) {
  29. // 在主视图里插入注释节点或者空白文本节点
  30. const placeholder = (n2.el = __DEV__
  31. ? createComment('teleport start')
  32. : createText(''))
  33. const mainAnchor = (n2.anchor = __DEV__
  34. ? createComment('teleport end')
  35. : createText(''))
  36. insert(placeholder, container, anchor)
  37. insert(mainAnchor, container, anchor)
  38. // 获取目标元素的 DOM 节点
  39. const target = (n2.target = resolveTarget(n2.props, querySelector))
  40. const targetAnchor = (n2.targetAnchor = createText(''))
  41. if (target) {
  42. insert(targetAnchor, target)
  43. // #2652 we could be teleporting from a non-SVG tree into an SVG tree
  44. isSVG = isSVG || isTargetSVG(target)
  45. } else if (__DEV__ && !disabled) {
  46. warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
  47. }
  48. const mount = (container: RendererElement, anchor: RendererNode) => {
  49. // Teleport *always* has Array children. This is enforced in both the
  50. // compiler and vnode children normalization.
  51. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  52. // 挂载子节点
  53. // (遍历 Teleport 组件的 children 属性,并逐一调用 patch 函数完成子节点的挂载)
  54. mountChildren(
  55. children as VNodeArrayChildren,
  56. container,
  57. anchor,
  58. parentComponent,
  59. parentSuspense,
  60. isSVG,
  61. slotScopeIds,
  62. optimized
  63. )
  64. }
  65. }
  66. if (disabled) {
  67. // disabled 则在原来的位置挂载
  68. mount(container, mainAnchor)
  69. } else if (target) {
  70. // 挂载在 target 元素的位置
  71. mount(target, targetAnchor)
  72. }
  73. }
  74. }

更新

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

  1. const process = (
  2. n1: TeleportVNode | null,
  3. n2: TeleportVNode,
  4. container: RendererElement,
  5. anchor: RendererNode | null,
  6. parentComponent: ComponentInternalInstance | null,
  7. parentSuspense: SuspenseBoundary | null,
  8. isSVG: boolean,
  9. slotScopeIds: string[] | null,
  10. optimized: boolean,
  11. internals: RendererInternals
  12. ) => {
  13. const {
  14. mc: mountChildren,
  15. pc: patchChildren,
  16. pbc: patchBlockChildren,
  17. o: { insert, querySelector, createText, createComment },
  18. } = internals
  19. // 是否禁用 Teleport
  20. const disabled = isTeleportDisabled(n2.props)
  21. let { shapeFlag, children, dynamicChildren } = n2
  22. // #3302
  23. // HMR updated, force full diff
  24. if (__DEV__ && isHmrUpdating) {
  25. optimized = false
  26. dynamicChildren = null
  27. }
  28. if (n1 == null) {
  29. // ---------------------------
  30. // 创建逻辑
  31. // ---------------------------
  32. } else {
  33. // update content
  34. n2.el = n1.el
  35. const mainAnchor = (n2.anchor = n1.anchor)!
  36. const target = (n2.target = n1.target)!
  37. const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  38. // 之前是不是 disabled 状态
  39. const wasDisabled = isTeleportDisabled(n1.props)
  40. const currentContainer = wasDisabled ? container : target
  41. const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  42. isSVG = isSVG || isTargetSVG(target)
  43. // 更新子节点
  44. if (dynamicChildren) {
  45. // fast path when the teleport happens to be a block root
  46. patchBlockChildren(
  47. n1.dynamicChildren!,
  48. dynamicChildren,
  49. currentContainer,
  50. parentComponent,
  51. parentSuspense,
  52. isSVG,
  53. slotScopeIds
  54. )
  55. // even in block tree mode we need to make sure all root-level nodes
  56. // in the teleport inherit previous DOM references so that they can
  57. // be moved in future patches.
  58. traverseStaticChildren(n1, n2, true)
  59. } else if (!optimized) {
  60. patchChildren(
  61. n1,
  62. n2,
  63. currentContainer,
  64. currentAnchor,
  65. parentComponent,
  66. parentSuspense,
  67. isSVG,
  68. slotScopeIds,
  69. false
  70. )
  71. }
  72. if (disabled) {
  73. if (!wasDisabled) {
  74. // enabled -> disabled
  75. // 把子节点从目标元素移动回主容器内
  76. moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE
  77. )
  78. }
  79. } else {
  80. // 目标容器改变
  81. // (to 属性值修改)
  82. // 新旧 to 属性值不同,则需要对内容进行移动
  83. if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
  84. // 获取新的目标元素
  85. const nextTarget = (n2.target = resolveTarget(n2.props, querySelector))
  86. if (nextTarget) {
  87. // 移动到新的目标元素
  88. moveTeleport(n2, nextTarget, null, internals, TeleportMoveTypes.TARGET_CHANGE)
  89. } else if (__DEV__) {
  90. warn('Invalid Teleport target on update:', target, `(${typeof target})`)
  91. }
  92. } else if (wasDisabled) {
  93. // disabled -> enabled
  94. // 移动到目标元素位置
  95. moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
  96. }
  97. }
  98. }
  99. }

Teleport 组件更新:

  • 更新子节点
    • 更新分为优化更新和普通的全量比对更新两种情况
  • 处理 disabled 属性变化的情况
    • 新节点 disabledtrue,且旧节点的 disabledfalse,则需要把 Teleport 的子节点从目标元素内部移回到主视图内
    • 新节点 disabledfalse
      • 先通过 to 属性是否改变来判断目标元素 target 有没有变化
        • 有变化,则把 Teleport 的子节点移动到新的 target 内部
        • 没变化
          • 则判断旧节点的 disabled 是否为 true
            • 是,则把 Teleport 的子节点从主视图内部移动到目标元素内部

移除

当组件移除的时候会执行 unmount 方法,它的内部会判断如果移除的组件是一个 Teleport 组件,就会执行组件的 remove 方法

  1. if (shapeFlag & 64 /* TELEPORT */) {
  2. vnode.type.remove(vnode, internals);
  3. }
  4. if (doRemove) {
  5. remove(vnode);
  6. }

Teleport 的 remove 首先通过 hostRemove 移除主视图渲染的 targetteleport start 注释节点以及 Teleport 主视图的元素 teleport end 注释节点,然后再去遍历 Teleport 的子节点执行 remove 移除,至此,Teleport 组件完成了移除。

  1. const remove = (
  2. vnode: VNode,
  3. parentComponent: ComponentInternalInstance | null,
  4. parentSuspense: SuspenseBoundary | null,
  5. optimized: boolean,
  6. { um: unmount, o: { remove: hostRemove } }: RendererInternals,
  7. doRemove: Boolean
  8. ) => {
  9. const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
  10. if (target) {
  11. hostRemove(targetAnchor!)
  12. }
  13. // an unmounted teleport should always remove its children if not disabled
  14. if (doRemove || !isTeleportDisabled(props)) {
  15. hostRemove(anchor!)
  16. if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
  17. for (let i = 0; i < (children as VNode[]).length; i++) {
  18. const child = (children as VNode[])[i]
  19. unmount(
  20. child,
  21. parentComponent,
  22. parentSuspense,
  23. true,
  24. !!child.dynamicChildren
  25. )
  26. }
  27. }
  28. }
  29. }