Vue 3.0 Teleport的使用和原理分析

5,628 阅读5分钟
Vue 3.0 系列文章

Vue 3.0组件的渲染流程

Vue 3.0组件的更新流程和diff算法详解

揭开Vue3.0 setup函数的神秘面纱

Vue 3.0 Props的初始化和更新流程的细节分析

Vue3.0 响应式实现原理分析

Vue 3.0 计算属性的实现原理分析

Vue3.0 常用响应式API的使用和原理分析(一)

Vue3.0 常用响应式API的使用和原理分析(二)

Vue 3.0 Provide和Inject实现共享数据

Vue 3.0 Teleport的使用和原理分析

Vue3侦听器和异步任务调度, 其中有个神秘角色

Vue3.0 指令

Vue3.0 内置指令的底层细节分析

Vue3.0 的事件绑定的实现逻辑是什么

Vue3.0 的双向绑定是如何实现的

Vue3.0的插槽是如何实现的?

探究Vue3.0的keep-alive和动态组件的实现逻辑

Vuex 4.x

Vue Router 4 的使用,一篇文章给你讲透彻

Vue3.0 新增了一个Teleport组件,开发者可以使用它将其所在组件模板的部分内容移动到特定的DOM位置,譬如body或者其他任意位置。

Vue 2.0要实现对应的功能则需要使用portal-vue三方库,或者使用$el操作DOM等来实现。

接下来我们就从使用方式和实现原理两个方面来分别介绍。

Teleport组件的使用

Teleport组件的使用很简单,把需要移动的内容包起来即可:
<teleport :to="body" :disabled="false">
    <div>需要移动的内容</div>
</teleport>

上面这些代码的表现结果是<div>需要移动的内容</div>会渲染在body上,而不是所在的组件的模板所在的位置。

Teleport有两个参数:

  1. to为需要移动的位置,可以是选择器也可以是DOM节点;
  2. disabled如果为true,内容不进行移动,disabled如果为false, 则Teleport包裹的元素节点会被移动到to的节点下。
例子:实现某部分内容在 组件的模板内子组件的模板内body 间切换。
  • 子组件有一个#teleport1节点
<!--SubContainer.vue-->

<template>
  <div id="teleport1">
    <h4>子组件</h4>
  </div>
</template>
  • APP组件包含子组件,有一个按钮button切换位置 和 需要传送的内容 <div class="send_content">{{ showingString }}</div>
<template>
    <sub-container />
    <button class="btn" @click="changePosition">传送门</button>
    <teleport :to="to" :disabled="disabled">
      <div class="send_content">{{ showingString }}</div>
    </teleport>
</template>

<script lang="ts">
import SubContainer from "./components/SubContainer.vue";
import { defineComponent, ref } from "vue";

enum TeleportPosition {
  currentInstance, // 当前组件
  subInstance, // 子组件
  body, // body
}

export default defineComponent({
  name: "App",
  components: {
    SubContainer,
  },
  setup() {
    // 位置
    let position = ref(TeleportPosition.currentInstance);
    // 显示的字符串内容
    let showingString = ref("内容显示在APP组件内");
    // 是否禁用teleport
    let disabled = ref(true);
    // 挂载的DOM节点
    let to = ref("body");

    // 切换位置
    let changePosition = () => {
      if (position.value == TeleportPosition.currentInstance) {
        position.value = TeleportPosition.subInstance;
        showingString.value = "内容显示在子组件内";
        disabled.value = false;
        to.value = "#teleport1";
      } else if (position.value == TeleportPosition.subInstance) {
        position.value = TeleportPosition.body;
        showingString.value = "内容显示在body内";
        disabled.value = false;
        to.value = "body";
      } else {
        position.value = TeleportPosition.currentInstance;
        showingString.value = "内容显示在APP组件内";
        disabled.value = true;
        to.value = "body";
      }
    };

    return { showingString, to, disabled, changePosition };
  },
});
</script>
  • 上面这些代码就实现了 <div class="send_content">{{ showingString }}</div> 这部分DOM内容可以在 APP组件的DOM节点,子组件的DOM节点 和 body 上选择挂载。

pic

Teleport组件的实现原理

Teleport组件的挂载

我们知道组件的挂载首先会进入patch函数:

<!-- render.ts -->
const patch: PatchFn = (
) => {
  // 省略其他...
  // 处理TELEPORT组件
  if (shapeFlag & ShapeFlags.TELEPORT) {
    ;(type as typeof TeleportImpl).process(
      n1 as TeleportVNode,
      n2 as TeleportVNode,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized,
      internals
    )
  }
}

patch函数执行时如果发现VNodeTeleport组件,则执行对应TeleportImplprocess方法。

// 1. 在主视图插入注释节点或者空白文本节点
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)
// 2. 获取目标元素节点
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = (n2.targetAnchor = createText(''))
if (target) {
  insert(targetAnchor, target)
  isSVG = isSVG || isTargetSVG(target)
}

const mount = (container: RendererElement, anchor: RendererNode) => {
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      children as VNodeArrayChildren,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
}

// 3. 在目标元素插入`Teleport`组件的子节点
if (disabled) {
  mount(container, mainAnchor)
} else if (target) {
  mount(target, targetAnchor)
}

具体逻辑如下:

  1. 创建一个节点mainAnchor, 开发环境下是一个注释节点,在发布环境是一个空文本节点, 将这个创建的mainAnchor节点挂载在父组件对应的DOM节点下;
  2. 使用querySelector找到Teleport组件to属性指定的节点target目标节点,然后在targetAnchor节点下创建一个空文本节点做为锚定节点;
  3. 如果Teleport组件disabled属性值为true,将Teleport组件的子节点挂载在mainAnchorh,如果disabled属性值为false,将Teleport组件的子节点挂载在目标节点targetAnchor

disable为真

disable为假

Teleport组件的更新

// 数据
n2.el = n1.el
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
const wasDisabled = isTeleportDisabled(n1.props)
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
isSVG = isSVG || isTargetSVG(target)

// 1. 更新子节点
if (dynamicChildren) {
  // fast path when the teleport happens to be a block root
  patchBlockChildren(
    n1.dynamicChildren!,
    dynamicChildren,
    currentContainer,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds
  )
  traverseStaticChildren(n1, n2, true)
} else if (!optimized) {
  patchChildren(
    n1,
    n2,
    currentContainer,
    currentAnchor,
    parentComponent,
    parentSuspense,
    isSVG,
    slotScopeIds,
    false
  )
}

// 根据disabled 和 to 进行分别操作
if (disabled) {
  if (!wasDisabled) {
    moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE)
  }
} else {
  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 (wasDisabled) {
    moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
  }
}

具体流程如下:

  1. 更新子节点,分为全量更新和优化更新;
  2. 如果新节点disabledtrue,而旧节点disabledfalse,把新节点移回到主视图节点mainAnchor;
  3. 如果新节点disabledfalseto节点有变化,则把新节点移动到to节点;
  4. 如果新节点disabledfalseto节点没有变化,如果旧节点disabledtrue, 新节点从到主视图节点移动到目标节点targetAnchor; 至此,更新节点完成。

Teleport组件的移除

我们知道组件的卸载首先会进入unmount方法:

if (shapeFlag & ShapeFlags.TELEPORT) {
  ;(vnode.type as typeof TeleportImpl).remove(
    vnode,
    parentComponent,
    parentSuspense,
    optimized,
    internals,
    doRemove
  )
} 

如果是Teleport组件,则直接调用TeleportImplremove方法;

  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
    
    // 1. 
    if (target) {
      hostRemove(targetAnchor!)
    }

    // an unmounted teleport should always remove its children if not disabled
    if (doRemove || !isTeleportDisabled(props)) {
      hostRemove(anchor!)
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        for (let i = 0; i < (children as VNode[]).length; i++) {
          const child = (children as VNode[])[i]
          unmount(
            child,
            parentComponent,
            parentSuspense,
            true,
            !!child.dynamicChildren
          )
        }
      }
    }
  }

具体流程如下:

  1. 如果有目标元素,则先移除目标元素;
  2. 移除主视图的元素;
  3. 移除子节点元素; 至此,移除节点完成。

一个思考题

<template>
    <button class="btn" @click="changePosition">传送门</button>
    <teleport :to="to" :disabled="disabled">
      <div class="send_content">{{ showingString }}</div>
    </teleport>
    <sub-container />
</template>

如果我们的案例中,子组件在Teleport组件的后面,此时Teleport组件是否能正常的显示?