美文网首页
Vue 3.0 Teleport的使用和原理分析

Vue 3.0 Teleport的使用和原理分析

作者: chonglingliu | 来源:发表于2022-01-16 22:14 被阅读0次

    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 上选择挂载。
    2.gif

    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组件是否能正常的显示?

    相关文章

      网友评论

          本文标题:Vue 3.0 Teleport的使用和原理分析

          本文链接:https://www.haomeiwen.com/subject/wtsqhrtx.html