前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3 Teleport组件原理分析

Vue3 Teleport组件原理分析

作者头像
玖柒的小窝
发布2021-12-08 12:49:33
5780
发布2021-12-08 12:49:33
举报
文章被收录于专栏:各类技术文章~各类技术文章~

前言

知其然而知其所以然,优秀的工程师不仅要能熟练的使用框架,还要了解其底层是如何实现的。本文主要探究Vue3源码中内置TelePort组件实现原理。

Teleport 组件常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。如果你要了解使用方法,官网已经介绍的很详细了,你可以点击查看 Vue3 Teleport

实现原理

Teleport 组件在源码中实现是一个对象,对外提供了几个方法,最主要的是两个 process 和 remove 方法。代码如下:

代码语言:javascript
复制
// packages/runtime-core/src/components/Teleport.ts
const Teleport = {
  __isTeleport: true,
  process(n1, n2, container, anchor, parentComponent, parentSuspense, 
  isSVG, optimized, internals) {
    if (n1 == null) {
      // 创建逻辑
    } else {
      // 更新逻辑
    }
  },

  remove(vnode, { r: remove, o: { remove: hostRemove } }) {
    // 删除逻辑
  },
  move: moveTeleport,
  hydrate: hydrateTeleport

}
复制代码

上面代码中,process 方法负责组件的创建和更新逻辑,remove 方法负责组件删除逻辑。

组件创建

模版渲染会最后会转换成render函数,再调用createBlock方法,最终组件的创建都会调用 patch 方法。

代码语言: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
  ) => {
    ...
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
      // 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
           // 处理文本节点
           break
      case Comment:
          // 处理注释节点
          break
      case Static:
         // 处理静态节点
         break
      case Fragment:
        // 处理 Fragment
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
          // 处理普通 DOM 元素
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          // 处理组件
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
          // 处理 TELEPORT
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
          // 处理 SUSPENSE
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }
    ...
  }
复制代码

可以看到,在 patch 阶段,会判断如果 type 是一个 Teleport 组件,则会执行它的 process 方法,接下来我们来看 process 方法关于 Teleport 组件创建部分的逻辑:

代码语言:javascript
复制
// packages/runtime-core/src/components/Teleport.ts
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
  ) {
    const {
      mc: mountChildren,
      pc: patchChildren,
      pbc: patchBlockChildren,
      o: { insert, querySelector, createText, createComment }
    } = internals

    const disabled = isTeleportDisabled(n2.props)
    let { shapeFlag, children, dynamicChildren } = n2
    ...
    if (n1 == null) {
      // insert anchors in the main view
      // 在主视图里插入注释节点或者空白文本节点
      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)
      // 获取目标移动的 DOM 节点
      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) {
        // 查找不到 target 则报警告
        warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
      }

      const mount = (container: RendererElement, anchor: RendererNode) => {
        // 挂载子节点
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(...)
        }
      }

      if (disabled) {
      // disabled 情况就在原先的位置挂载
        mount(container, mainAnchor)
      } else if (target) {
        // 挂载到 target 的位置
        mount(target, targetAnchor)
      }
    }  
  }
复制代码

Teleport 组件创建部分主要分为三个步骤,第一步在主视图里插入注释节点或者空白文本节点第二步获取目标元素节点第三步调用mount方法创建子节点往目标元素插入 Teleport 组件的子节点

组件更新

代码语言:javascript
复制
// packages/runtime-core/src/components/Teleport.ts
 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
  ) {
    const {
      mc: mountChildren,
      pc: patchChildren,
      pbc: patchBlockChildren,
      o: { insert, querySelector, createText, createComment }
    } = internals

    const disabled = isTeleportDisabled(n2.props)
    let { shapeFlag, children, dynamicChildren } = n2

    // #3302
    // HMR updated, force full diff
    if (__DEV__ && isHmrUpdating) {
      optimized = false
      dynamicChildren = null
    }

    if (n1 == null) {
      // 组件创建
    } else {
      // update content
      n2.el = n1.el
      const mainAnchor = (n2.anchor = n1.anchor)!
      const target = (n2.target = n1.target)!
      const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
      // 之前是不是 disabled 状态
      const wasDisabled = isTeleportDisabled(n1.props)
      const currentContainer = wasDisabled ? container : target
      const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
      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
        )
        traverseStaticChildren(n1, n2, true)
      } else if (!optimized) {
        patchChildren(
          n1,
          n2,
          currentContainer,
          currentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          false
        )
      }

      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) {
            // enabled -> disabled
            // 移动到新的目标元素
            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
          // 移动到目标元素位置
          moveTeleport(
            n2,
            target,
            targetAnchor,
            internals,
            TeleportMoveTypes.TOGGLE
          )
        }
      }
    }
  }
复制代码

Teleport 组件更新无非就是做几件事情:更新子节点,处理 disabled 属性变化的情况,处理 to 属性变化的情况。

首先,是更新 Teleport 组件的子节点,这里更新分为优化更新和普通的全量比对更新两种情况。

接着,是判断 Teleport 组件新节点配置 disabled 属性的情况,如果满足新节点 disabled 为 true,且旧节点的 disabled 为 false 的话,说明我们需要把 Teleport 的子节点从目标元素内部移回到主视图内部了。

如果新节点 disabled 为 false,那么先通过 to 属性是否改变来判断目标元素 target 有没有变化,如果有变化,则把 Teleport 的子节点移动到新的 target 内部;如果目标元素没变化,则判断旧节点的 disabled 是否为 true,如果是则把 Teleport 的子节点从主视图内部移动到目标元素内部了。

组件移除

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

代码语言:javascript
复制
 // packages/runtime-core/src/renderer.ts
 const unmount: UnmountFn = (
    vnode,
    parentComponent,
    parentSuspense,
    doRemove = false,
    optimized = false
  ) => {
    const {
      type,
      props,
      ref,
      children,
      dynamicChildren,
      shapeFlag,
      patchFlag,
      dirs
    } = vnode
      ...
      if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(vnode.type as typeof TeleportImpl).remove(
          vnode,
          parentComponent,
          parentSuspense,
          optimized,
          internals,
          doRemove
        )
      } 
      ...
  }
复制代码

可以看到,在 unmount 阶段,会判断如果 ShapeFlags 是一个 Teleport 组件,则会执行它的 remove 方法,接下来我们来看 remove 方法关于 Teleport 组件删除逻辑:

代码语言:javascript
复制
// packages/runtime-core/src/components/Teleport.ts
 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 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
          )
        }
      }
    }
  },
复制代码

Teleport 的 remove 方法实现很简单,首先通过 hostRemove 移除主视图渲染的锚点 teleport start 注释节点,然后再去遍历 Teleport 的子节点执行 unmount 移除。

执行完 Teleport 的 unmount 方法,会执行 hostRemove 方法移除 Teleport 主视图的元素 teleport end 注释节点,至此,Teleport 组件完成了移除。

最后

【想要快速搭建自己的前端静态博客,欢迎查阅Vuepress 快速搭建博客--一款你值得拥有的博客主题。】

参考文档

Vue.js 3.0 核心源码内参

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 实现原理
  • 组件创建
  • 组件更新
  • 组件移除
  • 最后
  • 参考文档
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档