likes
comments
collection
share

「Vue3学习篇」-Teleport

作者站长头像
站长
· 阅读数 21

『引言』

今天来介绍一个内置组件Teleport。

通常情况下,Vue组件的模板会被渲染在位于组件根元素内部的一段HTML代码。

但是,有时候可能需要将组件的内容渲染到DOM树中的其他位置,而不是组件自身的根元素内部。

这时,Teleport组件就派上用场了。

『Teleport』

『定义』

【官方解释】 将其插槽内容渲染到DOM中的另一个位置。

【我的理解】 可以理解成一个「任意门」,主要是可以把数据传送到另一个地方。

『为什么使用?』

经常看到很多用如下的例子来解释Teleport的用法:

例如:当在子组件中使用Dialog组件时,Dialog组件就被渲染到一层层子组件内部。导致嵌套组件的定位和样式难处理的问题。

明明Dialog组件应是一个独立的组件,现在需要在组件内部使用Dialog组件,同时要求渲染的DOM结构不嵌套在组件的DOM中,这该怎么办❓

🙋🙋‍♂️提问🚩:在实际开发中有没有遇到上面👆这种情况😎❓

对于上述这种情况可以使用<Teleport>包裹Dialog,此时就建立了一个传送门,可以将Dialog渲染的内容传送到任何指定的地方。

综上所述,使用Teleport 使得管理DOM更容易,开发更高效。

接下来看看 Teleport 的使用方式。

『用法』

基本使用Teleport 接收一个 to prop 来指定传送的目标。to 的值可以是一个 CSS 选择器字符串,也可以是一个 DOM 元素对象。

禁用 Teleport:通过对 Teleport 动态地传入一个 disabled prop 来处理。

  <teleport :disabled="true" to='body'>
     <A></A>
  </teleport>

使用disabled设置为true,则to属性不生效,false则生效

官网实例

『Teleport源码』

export const Teleport = TeleportImpl as unknown as {
  __isTeleport: true
  new (): {
    $props: VNodeProps & TeleportProps
    $slots: {
      default(): VNode[]
    }
  }
}

上述代码当中可以看到,Teleport是一个TeleportImpl对象。

接下来看一下,TeleportImpl对象都有包含什么❓

export const TeleportImpl = {
    __isTeleport: true,
    process() {
        if (n1 == null) {
            // 创建逻辑
        } else {
            // 更新逻辑
        }
    },
    remove() {
        // 卸载逻辑
    },
    // 移动节点逻辑
    move: moveTeleport,
    // 服务端渲染时 teleport 的特殊处理逻辑
    hydrate: hydrateTeleport
}

上述代码是对源码进行了精简,从代码中可以看出,TeleportImpl对象包含一个属性四个方法

『TeleportImpl对象的一个属性』

先来看一下TeleportImpl对象中的一个属性__isTeleport__isTeleport 属性是 Teleport 组件独有的特性,用作标识,固定为true。

__isTeleport会通过暴露一个 isTeleport方法,用来判断虚拟节点的节点类型是不是 Teleport类型,如果是,会将节点类型信息编码标记为ShapeFlags.TELEPORT

export const isTeleport = (type: any): boolean => type.__isTeleport

『TeleportImpl对象的四个方法』

  • process 负责组件的创建或者更新逻辑
  • remove 负责组件的删除逻辑
  • move 负责组件的移动逻辑
  • hydrate 负责同构渲染过程中的客户端激活

具体来了解一下这四个方法。

先看一下Teleport中最重要的一个方法:process方法,都知道Teleport组件需要渲染器的底层支持,那么……

🙋🙋‍♂️提问🚩:都知道渲染组件是由渲染器完成,为什么Teleport组件的渲染逻辑要从渲染器中分离出来实现其渲染逻辑❓🤔🤔

回答📒:

  • 可以避免渲染器逻辑代码 “膨胀”
  • 当没有使用Teleport组件时,由于Teleport的渲染逻辑分离出来,因此可以利用Tree-Shaking机制在最终的bundle中删除Teleport相关的代码,使得最终构建包的体积变小。

『process』

 process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    namespace: ElementNamespace,
    slotScopeIds: string[] | null,
    optimized: boolean,
    internals: RendererInternals,
  ) {
    // 省略部分代码
    if (n1 == null) 
     // 创建逻辑 
     // 省略部分代码
     } else {  
     // 更新逻辑 
     // 省略部分代码
     }
   }

以上就是process方法的精简代码,现在再来看看省略的部分代码,先是如下⬇️源码:

const {
      mc: mountChildren,
      pc: patchChildren,
      pbc: patchBlockChildren,
      o: { insert, querySelector, createText, createComment },
    } = internals

参数说明:

  • mc用来挂载子节点
  • pc用来更新节点
  • pbc用来更新块节点
  • o作为渲染器的配置项,提供了插入节点、查询选择器、创建文本节点、创建注释节点四个功能。

创建好之后,会调用isTeleportDisabled方法判断Teleportdisabled的值,从而知道Teleport是否被禁用了。

源码如下⬇️:

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

isTeleportDisabled源码如下⬇️:

  const isTeleportDisabled = (props: VNode['props']): boolean =>
  props && (props.disabled || props.disabled === '')

由于在热更新时,会出现重复挂载/卸载的问题,所以这里会进行一下判断,并且将optimized = false dynamicChildren = null,源码如下⬇️:

if (__DEV__ && isHmrUpdating) {
    optimized = false
    dynamicChildren = null
 }

🙋🙋‍♂️提问🚩:如何避免热更新带来的问题❓🤔🤔

回答📒:可以通过走全量diff来避免这个问题。

注意⚠️: 对应issure可以查看HMR adds changes twice when using teleport · Issue #3302 · vuejs/core

下面重点看看 Teleport.process 方法做了什么❓

首先会判断旧节点,当旧节点为null,不存在时,就会创建节点。

if (n1 == null) {
    //省略部分代码
 }

『创建节点』

  • 在创建过程中,首先判断是否是生产环境
  • 如果是的话,创建注释节点
  • 如果不是的话,创建两个空文本节点
  • 之后会向teleport组件存在的地方插入注释节点作为锚点,如果teleport被禁用以这个锚点为参照挂载在它前面
 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)
  • 其次会调用resolveTarget函数,通过props.to属性的值获取目标节点target,获取之后会创建一个目标节点的锚点targetAnchor(空文本元素)
   const target = (n2.target = resolveTarget(n2.props, querySelector))
   const targetAnchor = (n2.targetAnchor = createText(''))

resolveTarget函数源码如下⬇️:

const resolveTarget = <T = RendererElement>(
  props: TeleportProps | null,
  select: RendererOptions['querySelector'],
): T | null => {
  const targetSelector = props && props.to
  if (isString(targetSelector)) {
    if (!select) {
      __DEV__ &&
        warn(
          `Current renderer does not support string target for Teleports. ` +
            `(missing querySelector renderer option)`,
        )
      return null
    } else {
      const target = select(targetSelector)
      if (!target) {
        __DEV__ &&
          warn(
            `Failed to locate Teleport target with selector "${targetSelector}". ` +
              `Note the target element must exist before the component is mounted - ` +
              `i.e. the target cannot be rendered by the component itself, and ` +
              `ideally should be outside of the entire Vue component tree.`,
          )
      }
      return target as T
    }
  } else {
    if (__DEV__ && !targetSelector && !isTeleportDisabled(props)) {
      warn(`Invalid Teleport target: ${targetSelector}`)
    }
    return targetSelector as T
  }
}
  • 然后会判断目标节点target是否存在
  • 如果存在,则将目标节点target插入到锚点targetAnchor上,后面以这个锚点为参照
  • 如果不存在,开发环境就会抛出警告⚠️信息
  • 在这个过程中还会调用isTargetSVGisTargetMathML目标节点target进行判断
 if (target) {
    insert(targetAnchor, target)
    if (namespace === 'svg' || isTargetSVG(target)) {
      namespace = 'svg'
    } else if (namespace === 'mathml' || isTargetMathML(target)) {
      namespace = 'mathml'
    }
  } else if (__DEV__ && !disabled) {
    warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
  }

isTargetSVG源码如下⬇️:

  const isTargetSVG = (target: RendererElement): boolean =>
  typeof SVGElement !== 'undefined' && target instanceof SVGElement

isTargetMathML源码如下⬇️:

  const isTargetMathML = (target: RendererElement): boolean =>
  typeof MathMLElement === 'function' && target instanceof MathMLElement
  • 之后定义mount方法,会判断挂载的新节点(n2)的类型,如果是数组类型的子节点,则调用渲染器内部的mountChildren方法挂载。
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(
        children as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
     }
   }

看到这里会不会产生一点疑问。

🙋🙋‍♂️提问🚩:为什么Teleport组件的子节点必须是数组类型,为什么要定义一个函数❓🤔🤔

回答📒:

  1. 在源码的注释中可以看到,子节点必须是数组类型,且会被强制运用于编译器和虚拟子节点的标准化中。如果不是一个数组,Vue也会强制变成一个数组的。
  2. 至于为什么定义函数,其实是封装了一个函数,主要是因为在Teleport禁用和没禁用时,传入的参数不同。

mountChildren源码如下⬇️:

const mountChildren: MountChildrenFn = (
    children,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
    optimized,
    start = 0,
  ) => {
    for (let i = start; i < children.length; i++) {
      const child = (children[i] = optimized
        ? cloneIfMounted(children[i] as VNode)
        : normalizeVNode(children[i]))
      patch(
        null,
        child,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    }
  }
  • 最后会通过对disabled变量的判断,来决定是挂载在原先的位置,还是挂载在目标位置。
  • 如果为true禁用了,就调用mount方法挂载到container原先的位置。
  • 如果为false没禁用,就调用mount方法挂载到target目标位置。
 if (disabled) {
    mount(container, mainAnchor)
  } else if (target) {
    mount(target, targetAnchor)
  }

到这里整个创建过程就结束🔚了。

接下来就是当旧节点不为null,存在时,就是更新节点的过程。

『更新节点』

  • 首先是一些初始化,将旧节点中绑定的元素、锚点targetAnchor和目标节点target直接赋值给新节点
  n2.el = n1.el
  // !是 ts 的非空断言
  const mainAnchor = (n2.anchor = n1.anchor)!
  const target = (n2.target = n1.target)!
  const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
  • 调用isTeleportDisabled方法,判断Teleport组件是否禁用了,根据disabled属性的值判断目标容器和锚点
  • 如果禁用,挂载点就是周围父组件,否则就是to指定的目标挂载点
  • 同样的也会通过调用isTargetSVGisTargetMathML目标节点target进行判断,判断目标挂载点是否是SVG标签元素,是否是MathML元素

注意⚠️:MathML是一个用于描述数学公式、符号的一种XML标记语言。

  const wasDisabled = isTeleportDisabled(n1.props)
  const currentContainer = wasDisabled ? container : target
  const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
  if (namespace === 'svg' || isTargetSVG(target)) {
    namespace = 'svg'
  } else if (namespace === 'mathml' || isTargetMathML(target)) {
    namespace = 'mathml'
  }
  • 然后会判断需要更新的节点中是否存在动态子节点dynamicChildren
  • 如果存在,调用patchBlockChildren函数只对动态子节点进行更新
 if (dynamicChildren) {
    patchBlockChildren(
      n1.dynamicChildren!,
      dynamicChildren,
      currentContainer,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
    )
    traverseStaticChildren(n1, n2, true)
  } 

patchBlockChildren源码如下⬇️:

const patchBlockChildren: PatchBlockChildrenFn = (
    oldChildren,
    newChildren,
    fallbackContainer,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
  ) => {
    for (let i = 0; i < newChildren.length; i++) {
      const oldVNode = oldChildren[i]
      const newVNode = newChildren[i]
      const container =
        oldVNode.el &&
        (oldVNode.type === Fragment ||
          !isSameVNodeType(oldVNode, newVNode) ||
          oldVNode.shapeFlag & (ShapeFlags.COMPONENT | ShapeFlags.TELEPORT))
          ? hostParentNode(oldVNode.el)!
          : fallbackContainer
      patch(
        oldVNode,
        newVNode,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        true,
      )
    }
  }

🙋🙋‍♂️提问🚩:只对动态子节点进行更新,静态节点不更新,那静态节点怎么处理❓🤔🤔

回答📒:热更新后,为了让静态节点一直维持之前的层级结构,通过调用traverseStaticChildren方法做一些处理

traverseStaticChildren源码如下⬇️,主要就是对n1中静态的子节点做了一个继承

export function traverseStaticChildren(n1: VNode, n2: VNode, shallow = false) {
  const ch1 = n1.children
  const ch2 = n2.children
  if (isArray(ch1) && isArray(ch2)) {
    for (let i = 0; i < ch1.length; i++) {
      const c1 = ch1[i] as VNode
      let c2 = ch2[i] as VNode
      if (c2.shapeFlag & ShapeFlags.ELEMENT && !c2.dynamicChildren) {
        if (c2.patchFlag <= 0 || c2.patchFlag === PatchFlags.NEED_HYDRATION) {
          c2 = ch2[i] = cloneIfMounted(ch2[i] as VNode)
          c2.el = c1.el
        }
        if (!shallow) traverseStaticChildren(c1, c2)
      }
      if (c2.type === Text) {
        c2.el = c1.el
      }
      if (__DEV__ && c2.type === Comment && !c2.el) {
        c2.el = c1.el
      }
    }
  }
}
  • 如果不存在,先会判断是否开启优化模式,没有开启优化模式,就会调用patchChildren函数走全量diff来更新子节点,开启了,则不做任何操作。
  • 这里也就是在创建节点时需要对热更新进行判断的原因。
else if (!optimized) {
    patchChildren(
      n1,
      n2,
      currentContainer,
      currentAnchor,
      parentComponent,
      parentSuspense,
      namespace,
      slotScopeIds,
      false,
    )
  }

patchChildren源码如下⬇️:

const patchChildren: PatchChildrenFn = (
    n1,
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace: ElementNamespace,
    slotScopeIds,
    optimized = false,
  ) => {
    const c1 = n1 && n1.children
    const prevShapeFlag = n1 ? n1.shapeFlag : 0
    const c2 = n2.children

    const { patchFlag, shapeFlag } = n2
    if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
        patchUnkeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        return
      }
    }
    if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
      }
      if (c2 !== c1) {
        hostSetElementText(container, c2 as string)
      }
    } else {
      if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          patchKeyedChildren(
            c1 as VNode[],
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        } else {
          unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
        }
      } else {
        if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
          hostSetElementText(container, '')
        }
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(
            c2 as VNodeArrayChildren,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            namespace,
            slotScopeIds,
            optimized,
          )
        }
      }
    }
  }
  • 最后就是对todisabled两个属性变化的更新,分为了不同的情况
  1. 新节点的disabled属性为true,老节点的disabled属性为false,调用moveTeleport方法,直接将新节点移动到原始容器上。
 if (disabled) {
    if (!wasDisabled) {
      moveTeleport(
        n2,
        container,
        mainAnchor,
        internals,
        TeleportMoveTypes.TOGGLE,
      )
    } 
  1. 新、老节点的disabled属性都为true,就需判断to属性的变化。
    • 如果to发生改变,则将n1的props.to赋值给n2的props.to

      目的:当teleport变成enabled时,防止to属性没更新导致错误渲染

else {
   if (n2.props && n1.props && n2.props.to !== n1.props.to) {
     n2.props.to = n1.props.to
    }
  }
  1. 新节点的disabled属性为false,需要判断to属性,还会判断新节点是否存在,不存在开发环境就会报出警告⚠️信息。

  2. to属性发生变化,先获取新的目标容器,调用moveTeleport方法,将新节点移动新的目标节点。

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 (__DEV__) {
        warn(
          'Invalid Teleport target on update:',
          target,
          `(${typeof target})`,
        )
      }
    }
  1. to属性没变化,调用moveTeleport方法,将新节点移动到目标上。
else if (wasDisabled) {
  moveTeleport(
    n2,
    target,
    targetAnchor,
    internals,
    TeleportMoveTypes.TOGGLE,
  )
}

以上就是process方法做的所有的事情。接下来看一下remove方法具体做了什么❓

『remove』

渲染器中移除组件的方法unmount源码这里查看

Teleport组件会调用remove方法来卸载它本身。

remove(
    vnode: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    optimized: boolean,
    { um: unmount, o: { remove: hostRemove } }: RendererInternals,
    doRemove: boolean,
  ) {
    // 省略部分代码
  },
}
  • 首先会判断是否存在目标节点target,存在的话,移除目标节点target挂载的锚点节点 targetAnchor
 const { shapeFlag, children, anchor, targetAnchor, target, props } = vnode
    if (target) {
      hostRemove(targetAnchor!)
    }
  • 接着会去移除Teleport锚点节点anchor(即process中生成的注释节点)
 doRemove && hostRemove(anchor!)
  • 然后会遍历子节点,调用unmount方法使用递归的方式将Teleport的子节点全部删除
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,
    )
  }
}

『move』

渲染器的移动节点方法move源码这里查看

在组件内移除的方法是moveTeleport

export const TeleportImpl = {
  name: 'Teleport',
  __isTeleport: true,
  process(
  },
  remove(
  },
  move: moveTeleport,
}

process方法更新节点时就用到了moveTeleport

moveTeleport源码如下⬇️:

function moveTeleport(
  vnode: VNode,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER,
) {
  //省略部分代码
}

moveTeleport中有一个枚举值TeleportMoveTypes,会根据TeleportMoveTypes判断节点的移动类型。

export enum TeleportMoveTypes {
  TARGET_CHANGE,
  TOGGLE, // enable / disable
  REORDER, // moved in the main view
}

注意⚠️:在DOM diff过程中,对于一个非新增的节点,当没有最长递增子序列或者当前的节点索引不在最长递增子序列中的情况,就需要移动该节点。

所以REORDER类型是指teleport子节点在diff时不在最长递增子序列里面情况下的节点,需要移动。

  • 首先会判断节点移动的类型是否是TARGET_CHANGE类型,即目标节点target是否有变更
  • TARGET_CHANGE类型的话,调用渲染器的insert方法,将目标节点的锚点targetAnchor插入到container
if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
    insert(vnode.targetAnchor!, container, parentAnchor)
 }
  • 不是TARGET_CHANGE类型的话,会接着判断节点移动的类型是否是REORDER类型
  • REORDER类型的话,同样调用渲染器的insert方法,将对应元素el插入到container
const { el, anchor, shapeFlag, children, props } = vnode
  const isReorder = moveType === TeleportMoveTypes.REORDER
  if (isReorder) {
    insert(el!, container, parentAnchor)
  }
  • 不是REORDER类型的话,或者Teleport被禁用,遍历子节点,调用渲染器的move方法,将所有的子节点移动到container
if (!isReorder || isTeleportDisabled(props)) {
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      for (let i = 0; i < (children as VNode[]).length; i++) {
        move(
          (children as VNode[])[i],
          container,
          parentAnchor,
          MoveType.REORDER,
        )
      }
    }
  }
  • 最后再对节点移动的类型是REORDER节点的锚点节点(注释节点),调用渲染器的insert方法,将anchor插入到container中,完成移动
  if (isReorder) {
    insert(anchor!, container, parentAnchor)
  }

从源码中可知,将 Teleport 组件移动到目标挂载点中,事实上就是调用渲染器内部的方法 insert 和 move 来实现子节点的插入和移动。

『hydrate』

用于在服务器端渲染Teleport组件的方法是hydrateTeleport

export const TeleportImpl = {
  name: 'Teleport',
  __isTeleport: true,
  process(
  },
  remove(
  },
  ……
  hydrate: hydrateTeleport,
}

hydrateTeleport源码如下⬇️:

function hydrateTeleport(
  node: Node,
  vnode: TeleportVNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  slotScopeIds: string[] | null,
  optimized: boolean,
  {
    o: { nextSibling, parentNode, querySelector },
  }: RendererInternals<Node, Element>,
  hydrateChildren: (
    node: Node | null,
    vnode: VNode,
    container: Element,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => Node | null,
): Node | null {
  //省略部分代码
  if (target) {
     //省略部分代码
    if (vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        //省略部分代码
      }
    }
    updateCssVars(vnode)
  }
  //省略部分代码
}
  • 首先会通过resolveTarget,获取目标节点target
const target = (vnode.target = resolveTarget<Element>(
    vnode.props,
    querySelector,
  ))
  • resolveTarget是校验属性值是否合法,如果合法则返回该值 resolveTarget源码如下⬇️:
const resolveTarget = <T = RendererElement>(
  props: TeleportProps | null,
  select: RendererOptions['querySelector'],
): T | null => {
  const targetSelector = props && props.to
  if (isString(targetSelector)) {
  // 对targetSelector是否合法的校验代码
    if (!select) {
      __DEV__ &&
        warn(
          `Current renderer does not support string target for Teleports. ` +
            `(missing querySelector renderer option)`,
        )
      return null
    } else {
      const target = select(targetSelector)
      if (!target) {
        __DEV__ &&
          warn(
            `Failed to locate Teleport target with selector "${targetSelector}". ` +
              `Note the target element must exist before the component is mounted - ` +
              `i.e. the target cannot be rendered by the component itself, and ` +
              `ideally should be outside of the entire Vue component tree.`,
          )
      }
      return target as T
    }
  } else {
    if (__DEV__ && !targetSelector && !isTeleportDisabled(props)) {
      warn(`Invalid Teleport target: ${targetSelector}`)
    }
    return targetSelector as T
  }
}
  • 之后如果获取到的目标节点target存在,并且校验合法,接着获取目标节点的尾节点_lpa,或者不存在尾节点,才获取首节点
const targetNode =
   (target as TeleportTargetElement)._lpa || target.firstChild
  • 然后会判断是否开启disabled,开启了就激活下一个兄弟子节点
if (isTeleportDisabled(vnode.props)) {
  vnode.anchor = hydrateChildren(
    nextSibling(node), 
    vnode,
    parentNode(node)!,
    parentComponent,
    parentSuspense,
    slotScopeIds,
    optimized
  )
  vnode.targetAnchor = targetNode
}
  • 没开启即disabled为false,就先找到目标节点的锚点节点即注释节点,遍历获取目标节点的锚点节targetAnchor
let targetAnchor = targetNode
while (targetAnchor) {
  targetAnchor = nextSibling(targetAnchor)
  if (
    targetAnchor &&
    targetAnchor.nodeType === 8 &&
    (targetAnchor as Comment).data === 'teleport anchor'
  ) {
    vnode.targetAnchor = targetAnchor
    ;(target as TeleportTargetElement)._lpa =
      vnode.targetAnchor && nextSibling(vnode.targetAnchor as Node)
    break
  }
}
  • 然后再激活目标节点targetNode
hydrateChildren(
  targetNode, 
  vnode,        
  target,
  parentComponent,
  parentSuspense,
  slotScopeIds,
  optimized
)
  • 最后根据锚点节点判断是否返回下一个需要处理的兄弟节点,有利于后面节点的激活
return vnode.anchor && nextSibling(vnode.anchor as Node)

runtime-core/src/hydration.ts中的hydrateNode函数里调用了hydrateHydrateNode函数就是用来客户端激活的。

hydrateNode源码查看这里

Hydrate函数是vuejs进行同构渲染过程中对Teleport节点的激活处理。

转载自:https://juejin.cn/post/7363209007828189199
评论
请登录