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、参考资料

相关推荐
四喜花露水27 分钟前
Vue 自定义icon组件封装SVG图标
前端·javascript·vue.js
前端Hardy36 分钟前
HTML&CSS: 实现可爱的冰墩墩
前端·javascript·css·html·css3
web Rookie1 小时前
JS类型检测大全:从零基础到高级应用
开发语言·前端·javascript
Au_ust1 小时前
css:基础
前端·css
帅帅哥的兜兜1 小时前
css基础:底部固定,导航栏浮动在顶部
前端·css·css3
yi碗汤园1 小时前
【一文了解】C#基础-集合
开发语言·前端·unity·c#
就是个名称1 小时前
购物车-多元素组合动画css
前端·css
编程一生2 小时前
回调数据丢了?
运维·服务器·前端
丶21362 小时前
【鉴权】深入了解 Cookie:Web 开发中的客户端存储小数据
前端·安全·web
Missmiaomiao3 小时前
npm install慢
前端·npm·node.js