「Vue系列」欢迎传送到“Teleport”星球
创始人
2024-02-15 10:28:25

前言

大家好,我是落叶小小少年,我一直谨记学习不断,分享不停,输入的最好方式是输出,我始终相信

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

之前有学习并写了KeepAlive组件的实现原理,后来打算也把Teleport组件的原理也学习并记录下来,于是这几天便学习了下Teleport组件的实现原理,现在分享给大家,希望能和大家共同学习,进步

Tips: 这样面试的时候你就可以信心满满的向面试官讲解这个知识点了🫣

Teleport是什么

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

Teleport组件的功能

1. 不用有什么问题

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

在这里插入图片描述

这个MyModel组件是一个模态框,并且会被渲染到class为outer的的div标签下,但是我们通常希望这个模态框的蒙层能过遮挡页面上的任何元素

那么我们把这个组件的z-index设置的最高,但是问题是模态框的z-index会受限于它的容器元素,如果有其他元素与

重叠并有更高的 z-index,则它会覆盖住我们的模态框,所以我们自己实现这种效果就不太理想

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

2. 如何使用

Hello from the modal!

在这里插入图片描述

Teleport的to属性就是指定挂载的位置,上面我们会将

渲染到body上,而不会按照模板的dom层级渲染,于是就实现了dom的跨层级渲染

tips*: 如果to的目标元素是由Vue渲染的,那么必须确保在挂载 之前先挂载该元素

如何实现

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 } = n2switch (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 } = vnodeif (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

在这里插入图片描述

同时在解析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_CHILDRENchildren = [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// 判断要是否是disabledconst disabled = isTeleportDisabled(n2.props)// 解构shapeFlag, childrenconst { 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.elconst target = n2.target = n1.target// 获取到新的target元素const nextTarget = resolveTarget(n2.props, querySelector)// 先去patchChildren,更新childrenpatchChildren(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 | nulldisabled?: boolean
}
function resolveTarget(props: TeleportProps | null,select: RendererInternals['o']['querySelector']
) {const targetSelector = props && props.to// 简写,vue还会做警告信息处理和 null返回等// select函数就是querySelectorif (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 changeTOGGLE, // enable / disableREORDER // 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组件的主要代码进行分析讲解,当你知道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原理

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

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...