大家好,我是落叶小小少年,我一直谨记学习不断,分享不停,输入的最好方式是输出,我始终相信
之前有学习并写了KeepAlive组件的实现原理,后来打算也把Teleport组件的原理也学习并记录下来,于是这几天便学习了下Teleport组件的实现原理,现在分享给大家,希望能和大家共同学习,进步
Tips: 这样面试的时候你就可以信心满满的向面试官讲解这个知识点了
Teleport
是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去
想象一下如果你需要一个模态框的功能,这个组件的模板app组件内,但从整个应用试图的角度来看,它在 DOM 中应该被渲染在整个 Vue 应用外部的其他地方
假设我们有一个模态框,并且是下面这样的写法
这个MyModel组件是一个模态框,并且会被渲染到class为outer的的div标签下,但是我们通常希望这个模态框的蒙层能过遮挡页面上的任何元素
那么我们把这个组件的z-index设置的最高,但是问题是模态框的z-index会受限于它的容器元素,如果有其他元素与 于是就有了Teleport组件的,它的功能就行为了解决这类受限制的dom问题,它可以将组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去 Hello from the modal! Teleport的to属性就是指定挂载的位置,上面我们会将 tips*: 如果to的目标元素是由Vue渲染的,那么必须确保在挂载 Teleport组件在渲染的时候走组件内部的渲染,而不走通用的渲染逻辑,这需要渲染器的支持,也就是在 不了解挂载过程的可以去看Vue内置组件之KeepAlive原理里的组件的挂载过程 在patch函数里面判断是否是Teleport组件,如果是的话那么将渲染控制权交给Teleport组件,调用process函数去挂载children patch函数其中就包括mount和patch功能 修改 修改 编译器在编译Teleport组件时,在编译Teleport组件的vnode时会设置shapeFlag的值设置 同时在解析children的时候也会将其字节点编译成一个数组,而不像其他组件会被编译为插槽内容,所以在渲染字组件的时候只需要遍历数组就行 我们来简易实现Teleport组件的process挂载函数 process函数的实在渲染器的 patch函数里面调用的,那么我们知道patch函数主要的功能就是mount和patch功能,在Teleport组件里也是如此,要对n1和n2进行判处和处理 在container主视图中插入锚点信息没有特意写出来 如果是string类型则会通过document.querySelector去返回对应的dom元素,否则直接返回 所以在传递to的时候要考虑挂载id的话传递#xxx 当patch到Teleport组件时,也会走到渲染器里的move逻辑,那么Teleport组件的move逻辑是怎么实现的呢? 这里只写了移除子节点,其实还会移除主视图渲染的锚点(teleport start或注释节点以及teleport end或注释节点) 画了一个简单流程图方便大家一眼看明白整体流程 以上我们抽离Vue中的Teleport组件的主要代码进行分析讲解,当你知道Teleport组件的基本原理后使用上更加清晰明了 最后总结一下Teleport组件的实现原理: PS: 如果要想了解KeepAlive组件的实现原理,也可以看Vue内置组件之KeepAlive原理 如果对你有帮助的话,不妨点赞收藏关注一下,谢谢2. 如何使用
之前先挂载该元素如何实现
mount
、unmount
和move
的时候做特殊渲染处理简单实现(实现一个小而易懂的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
的渲染逻辑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组件
ShapeFlags.TELEPORT
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组件
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属性,返回是否是disabledfunction isTeleportDisabled(props: VNode['props']): boolean {
return props && (props.disabled || props.disabled === '')
}
resolveTarget
函数主要是获取props上的to属性,通过对to的判断,返回对应获取的dom或者是用户传递的dom// 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组件
// 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)
}
}
}
}
写在最后
ShapeFlags.TELEPORT
,然后在normalizeChildren的时候判断shapeFlag值,特殊处理children,将children设置为array