Vue3 源码解读-Teleport 组件实现原理


💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》

1、前言

Teleport组件是 Vue3 中内置的组件,可以将指定的内容渲染到特定的容器中,且不受 DOM 层级的限制,这在很多场景下非常有用,比如模态框、通知、弹出菜单等。例如,element-plus组件库中的 el-dialog组件底层就是使用 Teleport组件实现的,如下图所示:

2、Teleport 组件源码实现

2.1 实现原理

假如有以下模版内容:

html 复制代码
<template>
  <button @click="open = true">Open Modal</button>

  <Teleport to="body">
    <div v-if="open" class="modal">
      <p>Hello from the modal!</p>
      <button @click="open = false">Close</button>
    </div>
  </Teleport>
</template>

借助Vue SFC Playground平台,它会被编译成如下内容:

javascript 复制代码
return (_ctx, _cache) => {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("button", {
      onClick: _cache[0] || (_cache[0] = $event => (_isRef(open) ? open.value = true : open = true))
    }, "Open Modal"),
    (_openBlock(), _createBlock(_Teleport, { to: "body" }, [
      (_unref(open))
        ? (_openBlock(), _createElementBlock("div", _hoisted_1, [
            _hoisted_2,
            _createElementVNode("button", {
              onClick: _cache[1] || (_cache[1] = $event => (_isRef(open) ? open.value = false : open = false))
            }, "Close")
          ]))
        : _createCommentVNode("v-if", true)
    ]))
  ], 64 /* STABLE_FRAGMENT */))
}

可以看到,对于 Teleport标签,它是直接创建了 Teleport内置组件,我们来看它的实现:

javascript 复制代码
export const TeleportImpl = {
  name: 'Teleport',
  // Telport标识符
  __isTeleport: true,
  /**
   * 客户端渲染函数,优点:
   * 1.可以避免渲染器逻辑代码膨胀
   * 2.可以使用Tree-shaking删除代码,使得代码体积变小
   */
  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
  ) {
    // 创建逻辑
    if (n1 == null) {

    } else {
      // 更新逻辑
    }
  },
  // 删除逻辑
  remove(
    vnode: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    optimized: boolean,
    { um: unmount, o: { remove: hostRemove } }: RendererInternals,
    doRemove: boolean
  ) {

  },
  // 移动Teleport
  move: moveTeleport,
  // 服务端渲染Teleport
  hydrate: hydrateTeleport
}

Teleport 组件其实就是一个对象,对外提供了 processremovemove方法,它们的作用分别是:

  • process :组件的创建和更新逻辑。将该逻辑剥离的原因有:1、避免渲染器的代码过多,2、可以利用 Tree-shaking删除 Teleport相关的代码,减小代码体积;
  • remove:组件的删除逻辑;
  • move:组件的移动逻辑。

接下来我们就从这三个函数的实现流程来分析 Teleport 组件的实现原理。

2.2 创建流程

patch函数中对 process函数的调用如下:

javascript 复制代码
// packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {

    // 省略部分代码

    const { type, ref, shapeFlag } = n2
    switch (type) {
      // 省略部分代码
      default:

        // 省略部分代码

        // shapeFlag 的类型为 TELEPORT,则它是 Teleport 组件
        // 调用 Teleport 组件选项中的 process 函数将控制权交接出去
        // 传递给 process 函数的第五个参数是渲染器的一些内部方法
        else if (shapeFlag & ShapeFlags.TELEPORT) {
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        }
        // 省略部分代码
    }

    // 省略部分代码
  }

当判断 vnode的类型是 Teleport时,直接调用组件选项中定义的 process 方法将渲染控制权完全交接出去 ,这样就实现了渲染逻辑的分离。接下来我们来看看 Teleport中的 process方法实现逻辑:

Teleport 组件创建逻辑

以上代码处理流程如下:

  1. 创建并挂载注释节点

    1. 创建一个注释节点或文本节点作为占位符 placeholder,并将其赋值给 n2.el。如果是开发环境 (__DEV__true),则创建一个注释节点 "teleport start",否则创建一个空文本节点。

    2. 创建一个注释节点或文本节点作为锚点 mainAnchor,并将其赋值给 n2.anchor。如果是开发环境,创建一个注释节点 "teleport end",否则创建一个空文本节点。

    3. 使用 insert 函数将 placeholdermainAnchor 插入到 DOM 中的 container 元素里,位置是 anchor 指定的位置。

      第 1 步作用是:非生产环境往 Teleport 组件原本的位置插入注释节点,在生产环境插入空白文本节点

      typescript 复制代码
      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. 挂载 target 节点和占位节点

    1. 通过 resolveTarget 函数解析 Teleport 的目标元素,并将其赋值给 n2.target。同时创建一个空文本节点 targetAnchor 并赋值给 n2.targetAnchor

    2. 如果找到了有效的 target,则将 targetAnchor 插入到 target 中。如果在 SVG 上下文中,还会设置 isSVG 标志。

    3. 如果在开发环境中,且 target 无效且 disabled 未设置为 true,则会发出警告。

      第 2 步的作用是:拿到目标节点 target,并在目标节点位置创建了空的文本节点作为锚点

      typescript 复制代码
      // 获取容器,即挂载点
      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})`)
      }
  3. 定义渲染 Teleport 子节点的 mount 方法

    1. 定义 mount 函数,它接受 containeranchor 作为参数。如果 Teleport 的子节点是数组类型的子节点,则它会调用 mountChildren 函数来渲染这些子节点。

      第 3 步的作用是:定义渲染子节点的方法

      typescript 复制代码
      // 将n2.children渲染到指定挂载点
      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) {
          // 调用渲染器内部的 mountChildren 方法渲染 Teleport 组件的插槽内容
          mountChildren(
            children as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        }
      }
  4. 根据 disabled 处理不同的逻辑

    1. 如果 disabledtrue,则在当前的 containermainAnchor 上调用 mount 函数;

    2. 如果 disabledfalse 且存在有效的 target,则在 targettargetAnchor 上调用 mount 函数;

      第 4 步的作用是:根据 disabled 属性,实现真正的渲染子节点

      typescript 复制代码
      // 如果 Teleport 组件的 disabled 为 true,说明禁用了 <teleport> 的功能,Teleport 只会在 container 中渲染
      if (disabled) {
        mount(container, mainAnchor)
      } else if (target) {
        // 如果没有禁用 <teleport> 的功能,并且存在挂载点,则将其插槽内容渲染到target容中
        mount(target, targetAnchor)
      }

整体来看,这段代码负责创建 Teleport 组件的占位符和锚点,将它们插入到 DOM 中,并根据 disabled 属性决定是在原位置渲染子组件还是在 Teleport 的目标位置渲染子组件。

2.3 更新流程

Teleport 更新流程

typescript 复制代码
// update content
  n2.el = n1.el
  const mainAnchor = (n2.anchor = n1.anchor)!
  // 挂载点
  const target = (n2.target = n1.target)!
  // 锚点
  const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  // 判断Telport组件是否禁用了
  const wasDisabled = isTeleportDisabled(n1.props)
  // 如果禁用了,那么挂载点就是周围父组件,否则就是to指定的目标挂载点
  const currentContainer = wasDisabled ? container : target
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  // 目标挂载点是否是SVG标签元素
  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
      // move into main container
      moveTeleport(
        n2,
        container,
        mainAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    } else {
      // #7835
      // When `teleport` is disabled, `to` may change, making it always old,
      // to ensure the correct `to` when enabled
      if (n2.props && n1.props && n2.props.to !== n1.props.to) {
        n2.props.to = n1.props.to
      }
    }
  } else {
    // target changed
    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
      // move into teleport target
      moveTeleport(
        n2,
        target,
        targetAnchor,
        internals,
        TeleportMoveTypes.TOGGLE
      )
    }
  }
}

Teleport组件更新主要做了以下几件事情:更新子节点、处理 disabled属性变化、处理 to属性变化的情况。更新流程的处理步骤拆解如下:

  1. 更新子节点:可以分为优化更新和普通的全量对比更新两种情况;
  2. 处理 disabled属性变化的情况:如果满足新节点 disabledtrue,且旧节点的 disabledfalse的话,说明我们需要把 Teleport 的子节点从目标元素内部移回到主视图内部了,此时执行相应的处理逻辑;
  3. 处理 to属性变化的情况:如果新节点的 disabedtrue,那么先通过 to属性是否改变来判断目标元素 target 有没有变化,如果有变化,则把 Teleport的子节点移动到新的 target内部;如果目标元素没变化,则判断旧节点的 disabled 是否为 true,如果是则把 Teleport的子节点从主视图内部移动到目标元素内部了。

2.3 卸载流程

当组件卸载的时候会执行 umount方法,它的内部会判断卸载的组件是否是 Teleport,如果是则会执行 Teleport组件的 remove方法,如下代码所示:

umount 卸载 Teleport 组件

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

我们来看看 Teleport组件 remove方法的实现:

typescript 复制代码
// 移除Telport
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

  if (target) {
    hostRemove(targetAnchor!)
  }

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

卸载流程中,首先调用 hostRemove移除主视图渲染的锚点"teleport start"注释节点,然后再去遍历 Teleport 的子节点调用 remove方法移除子节点。

3、总结

本篇详细解析了 Teleport组件的使用场景,剖析了创建流程、更新流程以及卸载流程。总体流程如下:

4、参考资料

相关推荐
慧一居士1 小时前
flex 布局完整功能介绍和示例演示
前端
DoraBigHead1 小时前
小哆啦解题记——两数失踪事件
前端·算法·面试
一斤代码6 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子6 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年6 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子7 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina7 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路8 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说8 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409198 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app