前言

咱们好,我是落叶小小少年,我一向谨记学习不断,共享不停,输入的最好办法是输出,我一直相信

  • 用最核心代码更简略理解深的技术点
  • 用通俗易懂的话,讲难的常识点

之前有学习并写了KeepAlive组件的完成原理,后来打算也把Teleport组件的原理也学习并记录下来,所以这几天便学习了下Teleport组件的完成原理,现在共享给咱们,希望能和咱们一起学习,进步

Tips: 这样面试的时分你就能够决心满满的向面试官解说这个常识点了

Teleport是什么

Teleport 是一个内置组件,它能够将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去

Teleport组件的功用

1. 不用有什么问题

想象一下假如你需求一个模态框的功用,这个组件的模板app组件内,但从整个应用试图的角度来看,它在 DOM 中应该被烘托在整个 Vue 应用外部的其他地方 假设咱们有一个模态框,而且是下面这样的写法

<div class="outer">
  <MyModal />
</div>

「Vue系列」欢迎传送到“Teleport”星球

这个MyModel组件是一个模态框,而且会被烘托到class为outer的的div标签下,但是咱们一般希望这个模态框的蒙层能过遮挡页面上的任何元素

那么咱们把这个组件的z-index设置的最高,但是问题是模态框的z-index会受限于它的容器元素,假如有其他元素与 <div class="outer"> 堆叠并有更高的 z-index,则它会覆盖住咱们的模态框,所以咱们自己完成这种作用就不太抱负

所以就有了Teleport组件的,它的功用就行为了解决这类受限制的dom问题,它能够将组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去

2. 怎样运用

<Teleport to="body">
  <div class="modal">
    <p>Hello from the modal!</p>
  </div>
</Teleport>

「Vue系列」欢迎传送到“Teleport”星球
Teleport的to特点便是指定挂载的位置,上面咱们会将<div class=“modal”> 烘托到body上,而不会按照模板的dom层级烘托,所以就完成了dom的跨层级烘托

tips: 假如to的方针元素是由Vue烘托的,那么有必要确保在挂载 <Teleport> 之前先挂载该元素

怎样完成

Teleport组件在烘托的时分走组件内部的烘托,而不走通用的烘托逻辑,这需求烘托器的支持,也便是在mountunmountmove的时分做特别烘托处理

不了解挂载进程的能够去看Vue内置组件之KeepAlive原理里的组件的挂载进程

简略完成(完成一个小而易懂的Teleport组件)

1. Teleport 组件的特点

type TeleportProps = {
  to: string | RendererElement | null // string或许已烘托的方针元素
  disabled?: boolean
}
export const TeleportImpl = {
  // 用来标识是否是Teleport组件
  __isTeleport: true,
  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    internals: RendererInternals
  ) {
	// 这儿是烘托逻辑
  },
  remove(
    vnode: TeleportVNode, { o: { remove: hostRemove }, um: unmount   }) {
	// 这儿是毁掉逻辑
  }
  move(n2: TeleportVNode, container: RendererElement, anchor: RendererNode, internals: RendererInternals) {
	// 这儿是move逻辑,move被Teleport烘托的内容
  }

2.修正烘托器的烘托逻辑

2.1 修正patch的烘托逻辑

在patch函数里面判别是否是Teleport组件,假如是的话那么将烘托控制权交给Teleport组件,调用process函数去挂载children

patch函数其中就包括mount和patch功用

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null
  ) {
	if (n1 === n2) {
      return
    }
    if (n1 && !isSameVNodeType(n1, n2)) {
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }
    const { type, shapeFlag } = n2
	switch (type) {
	  case Text:
		//...
	  case Comment:
		//...
	  case Fragment:
		//...
	  default:
		// 经过shapeFlag进行判别,这个在解析Teleport组件时就设置
		if (shapeFlag & ShapeFlags.TELEPORT) {
			// 烘托时直接调用其process函数去烘托
		  ;(type as typeof TeleportImpl).process(
              n1 as TeleportVNode,
              n2 as TeleportVNode,
              container,
              anchor,
              internals
            )
	    }
	}
  }

2.2 修正move的烘托逻辑

修正move的烘托逻辑 在Teleport组件需求move的时分不需求走烘托器的move函数,而是将其拦截并进行一些处理比方挂载Text视图

const move: MoveFn = (
	vnode,
    container,
    anchor,
	internals
  ) {
	const { el, type, transition, children, shapeFlag } = vnode
	if (shapeFlag & ShapeFlags.TELEPORT) {
      ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
      return
    }
}

2.3 修正unmount的毁掉逻辑

修正unmount的烘托逻辑 当Teleport组件毁掉时,Teleport的字组件是有不同的毁掉逻辑的,所以在判别时Teleport组件时会调用对应的remove函数进行卸载

const unmount: UnmountFn = (
    vnode,
    parentComponent,
    parentSuspense,
    doRemove = false,
    optimized = false
) => {
	const {
      children,
      shapeFlag,
    } = vnode
	// ...
	if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(vnode.type as typeof TeleportImpl).remove(
          vnode,
          parentComponent,
          parentSuspense,
          optimized,
          internals,
          doRemove
        )
    }
}

3.编译Teleport组件

编译器在编译Teleport组件时,在编译Teleport组件的vnode时会设置shapeFlag的值设置ShapeFlags.TELEPORT

「Vue系列」欢迎传送到“Teleport”星球

一起在解析children的时分也会将其字节点编译成一个数组,而不像其他组件会被编译为插槽内容,所以在烘托字组件的时分只需求遍历数组就行

export function normalizeChildren(vnode: VNode, children: unknown) {
	const { shapeFlag } = vnode
    // ....
	if (children == null) {
       children = null
    } else if (isArray(children)) {
    	type = ShapeFlags.ARRAY_CHILDREN
	} else {
		// 确保Teleport的children一定为ARRAY_CHILDREN类型
		if (shapeFlag & ShapeFlags.TELEPORT) {
      	    type = ShapeFlags.ARRAY_CHILDREN
      	    children = [createTextVNode(children as string)]
    	}
	}
}

4. 挂载Teleport组件

咱们来简易完成Teleport组件的process挂载函数

process函数的真实烘托器的 patch函数里面调用的,那么咱们知道patch函数首要的功用便是mount和patch功用,在Teleport组件里也是如此,要对n1和n2进行判处和处理

在container主视图中刺进锚点信息没有特意写出来

  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    internals: RendererInternals
  ) {
	// 拿到烘托器的一些办法
    const {
      mc: mountChildren,
      pc: patchChildren,
      o: { insert, querySelector, createComment }
    } = internals
	// 判别要是否是disabled
    const disabled = isTeleportDisabled(n2.props)
	// 解构shapeFlag, children
    const { shapeFlag, children } = n2;
	// 挂载的场景
    if (n1 == null) {
	  // 在container主视图中刺进锚点信息
      const placeholder = (n2.el = __DEV__
        ? createComment('teleport start')
        : createText(''))
	  insert(placeholder, container, anchor)
  	  // ...teleport end锚点
	  // 经过props上的to获取到要挂载的target元素
      const target = (n2.target = resolveTarget(n2.props, querySelector));
	  // 被禁用
      if (disabled) {
		// 直接原地挂载,挂载到container上
        if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
		  // 上面说到在编译阶段关于Teleport会将children序列化为array类型
		  // 调用烘托器的mountChildren函数挂载到container上
          mountChildren(children as VNodeArrayChildren, container, anchor, internals);
        }
	   // 未被禁用
      } else {
        if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
		  // 调用烘托器的mountChildren函数挂载到target上
          mountChildren(children as VNodeArrayChildren, target, null, internals)
        }
      }
    // patch的场景
    } else {
	  // 简化,Vue还会有很多判别,比方从disabled到enabled
	  //enabled到disabled以及target相同的状况
      n2.el = n1.el
      const target = n2.target = n1.target
	  // 获取到新的target元素
      const nextTarget = resolveTarget(n2.props, querySelector)
	  // 先去patchChildren,更新children
      patchChildren(n1, n2, target, anchor, internals, false)
	  // 将patch后的n2 vnode直接move到新的target上即可
     // 下文会完成,便是move时调用的函数
      moveTeleport(n2, nextTarget, anchor, internals, TeleportMoveTypes.TARGET_CHANGE);
    }
  }

isTeleportDisabled函数首要是获取props上的disabled特点,回来是否是disabled

function isTeleportDisabled(props: VNode['props']): boolean {
  return props && (props.disabled || props.disabled === '')
}

resolveTarget函数首要是获取props上的to特点,经过对to的判别,回来对应获取的dom或许是用户传递的dom

假如是string类型则会经过document.querySelector去回来对应的dom元素,不然直接回来

所以在传递to的时分要考虑挂载id的话传递#xxx

// props类型
type TeleportProps = {
  to: string | RendererElement | null
  disabled?: boolean
}
function resolveTarget(
  props: TeleportProps | null,
  select: RendererInternals['o']['querySelector']
) {
  const targetSelector = props && props.to
  // 简写,vue还会做警告信息处理和 null回来等
  // select函数便是querySelector
  if (typeof targetSelector === 'string' && select) {
    return select(targetSelector)
  } else {
    return targetSelector as RendererElement
  }
}

5. 移动Teleport组件

当patch到Teleport组件时,也会走到烘托器里的move逻辑,那么Teleport组件的move逻辑是怎样完成的呢?

// Teleport组件move的类型
export const enum TeleportMoveTypes {
  TARGET_CHANGE,  // target change
  TOGGLE, // enable / disable
  REORDER // moved in the main view
}
function moveTeleport(vnode: VNode, container: RendererElement, parentAnchor: RendererNode, internals: RendererInternals, moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER) {
  const { o: { insert }, m: move } = internals;
  // 假如是target change,则直接移动target锚点
  if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
    insert(vnode.targetAnchor!, container, parentAnchor)
  }
  const { el, props, shapeFlag, anchor: _anchor, children } = vnode;
  const isReorder = moveType === TeleportMoveTypes.REORDER
  // 假如这是重新排序,则移动主视图锚点
  if (isReorder) {
    insert(el!, container, parentAnchor)
  }
   // 假如这是重新排序而且传送已启用(内容在方针中)不要移动孩子。
  // 所以相反的是:只有在这种状况下才移动孩子
  // 不是重新排序,或许传送被禁用
  if (!isReorder || isTeleportDisabled(props)) {
    if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
      for (let i = 0; i < children.length; i++) {
        move(children[i], container, parentAnchor, MoveType.REORDER)
      }
    }
  }
}

6. 毁掉Teleport组件

remove: (vnode: VNode, parentComponent: ComponentInternalInstance | null,
    optimized: boolean,
    { um: unmount, o: { remove: hostRemove } }: RendererInternals,
    doRemove: Boolean
  ) => {
    const { shapeFlag, children, anchor, props } = vnode;
	// 自动remove或许非disabled的状况下要将挂载的字节点毁掉
    if (doRemove || !isTeleportDisabled(props)) {
      hostRemove(anchor!)
      if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        for (let i = 0; i < children.length; i++) {
		  // 调用烘托器上的unmount函数
          unmount(children[i], parentComponent, null, true, true)
        }
      }
    }
  }

这儿只写了移除子节点,其实还会移除主视图烘托的锚点(teleport start或注释节点以及teleport end或注释节点)

画了一个简略流程图便利咱们一眼看理解全体流程

「Vue系列」欢迎传送到“Teleport”星球

写在最终

以上咱们抽离Vue中的Teleport组件的首要代码进行分析解说,当你知道Teleport组件的基本原理后运用上愈加明晰明了

最终总结一下Teleport组件的完成原理:

  1. 首先在编译Teleport组件的时分会在vnode的shapeFlag上做标记,将其设置为ShapeFlags.TELEPORT,然后在normalizeChildren的时分判别shapeFlag值,特别处理children,将children设置为array
  2. Teleport组件在挂载的时分调用patch烘托器的patch办法,然后在调用组件内的process函数,process函数则有组件创立和组件更新的逻辑
  3. 创立的时分会经过to特点拿到target元素,然后判别是否为disabled,假如是则不烘托到target上,不然就烘托到target上
  4. Teleport 组件创立首先会在在主视图里刺进注释节点或许空白文本节点,最终调用mountChildren办法创立子节点往target方针元素刺进 Teleport 组件的子节点
  5. Teleport 组件更新首先会更新子节点,处理 disabled 特点改变的状况,处理 to 特点改变的状况,决定要不要move子节点
  6. Teleport组件move的时分不是重新排序,或许传送被禁用的时分才move子节点
  7. 毁掉的时分Teleport组件会调用内部的remove函数,移除主视图烘托的锚点,一起判别在自动remove或许非disabled的状况下要将挂载的字节点毁掉

PS: 假如要想了解KeepAlive组件的完成原理,也能够看Vue内置组件之KeepAlive原理

假如对你有帮助的话,不妨点赞保藏关注一下,谢谢