💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》
1、前言
Teleport
组件是 Vue3 中内置的组件,可以将指定的内容渲染到特定的容器中,且不受 DOM 层级的限制,这在很多场景下非常有用,比如模态框、通知、弹出菜单等。例如,element-plus组件库中的 el-dialog
组件底层就是使用 Teleport
组件实现的,如下图所示:
2、Teleport 组件源码实现
2.1 实现原理
假如有以下模版内容:
html
<template>
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>
借助Vue SFC Playground平台,它会被编译成如下内容:
javascript
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = $event => (_isRef(open) ? open.value = true : open = true))
}, "Open Modal"),
(_openBlock(), _createBlock(_Teleport, { to: "body" }, [
(_unref(open))
? (_openBlock(), _createElementBlock("div", _hoisted_1, [
_hoisted_2,
_createElementVNode("button", {
onClick: _cache[1] || (_cache[1] = $event => (_isRef(open) ? open.value = false : open = false))
}, "Close")
]))
: _createCommentVNode("v-if", true)
]))
], 64 /* STABLE_FRAGMENT */))
}
可以看到,对于 Teleport
标签,它是直接创建了 Teleport
内置组件,我们来看它的实现:
javascript
export const TeleportImpl = {
name: 'Teleport',
// Telport标识符
__isTeleport: true,
/**
* 客户端渲染函数,优点:
* 1.可以避免渲染器逻辑代码膨胀
* 2.可以使用Tree-shaking删除代码,使得代码体积变小
*/
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
) {
// 创建逻辑
if (n1 == null) {
} else {
// 更新逻辑
}
},
// 删除逻辑
remove(
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
optimized: boolean,
{ um: unmount, o: { remove: hostRemove } }: RendererInternals,
doRemove: boolean
) {
},
// 移动Teleport
move: moveTeleport,
// 服务端渲染Teleport
hydrate: hydrateTeleport
}
Teleport
组件其实就是一个对象,对外提供了 process
、remove
、move
方法,它们的作用分别是:
- process :组件的创建和更新逻辑。将该逻辑剥离的原因有:1、避免渲染器的代码过多,2、可以利用
Tree-shaking
删除Teleport
相关的代码,减小代码体积; - remove:组件的删除逻辑;
- move:组件的移动逻辑。
接下来我们就从这三个函数的实现流程来分析 Teleport
组件的实现原理。
2.2 创建流程
在 patch
函数中对 process
函数的调用如下:
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
) => {
// 省略部分代码
const { type, ref, shapeFlag } = n2
switch (type) {
// 省略部分代码
default:
// 省略部分代码
// shapeFlag 的类型为 TELEPORT,则它是 Teleport 组件
// 调用 Teleport 组件选项中的 process 函数将控制权交接出去
// 传递给 process 函数的第五个参数是渲染器的一些内部方法
else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
}
// 省略部分代码
}
// 省略部分代码
}
当判断 vnode
的类型是 Teleport
时,直接调用组件选项中定义的 process
方法将渲染控制权完全交接出去 ,这样就实现了渲染逻辑的分离。接下来我们来看看 Teleport
中的 process
方法实现逻辑:
以上代码处理流程如下:
-
-
创建一个注释节点或文本节点作为占位符
placeholder
,并将其赋值给n2.el
。如果是开发环境 (__DEV__
为true
),则创建一个注释节点 "teleport start",否则创建一个空文本节点。 -
创建一个注释节点或文本节点作为锚点
mainAnchor
,并将其赋值给n2.anchor
。如果是开发环境,创建一个注释节点 "teleport end",否则创建一个空文本节点。 -
使用
insert
函数将placeholder
和mainAnchor
插入到 DOM 中的container
元素里,位置是anchor
指定的位置。第 1 步作用是:非生产环境往 Teleport 组件原本的位置插入注释节点,在生产环境插入空白文本节点。
typescriptconst 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
函数解析Teleport
的目标元素,并将其赋值给n2.target
。同时创建一个空文本节点targetAnchor
并赋值给n2.targetAnchor
。 -
如果找到了有效的
target
,则将targetAnchor
插入到target
中。如果在 SVG 上下文中,还会设置isSVG
标志。 -
如果在开发环境中,且
target
无效且disabled
未设置为true
,则会发出警告。第 2 步的作用是:拿到目标节点 target,并在目标节点位置创建了空的文本节点作为锚点。
typescript// 获取容器,即挂载点 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) { warn('Invalid Teleport target on mount:', target, `(${typeof target})`) }
-
-
-
定义
mount
函数,它接受container
和anchor
作为参数。如果Teleport
的子节点是数组类型的子节点,则它会调用mountChildren
函数来渲染这些子节点。第 3 步的作用是:定义渲染子节点的方法。
typescript// 将n2.children渲染到指定挂载点 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 方法渲染 Teleport 组件的插槽内容 mountChildren( children as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } }
-
-
-
如果
disabled
为true
,则在当前的container
和mainAnchor
上调用mount
函数; -
如果
disabled
为false
且存在有效的target
,则在target
和targetAnchor
上调用mount
函数;第 4 步的作用是:根据 disabled 属性,实现真正的渲染子节点。
typescript// 如果 Teleport 组件的 disabled 为 true,说明禁用了 <teleport> 的功能,Teleport 只会在 container 中渲染 if (disabled) { mount(container, mainAnchor) } else if (target) { // 如果没有禁用 <teleport> 的功能,并且存在挂载点,则将其插槽内容渲染到target容中 mount(target, targetAnchor) }
-
整体来看,这段代码负责创建 Teleport
组件的占位符和锚点,将它们插入到 DOM 中,并根据 disabled
属性决定是在原位置渲染子组件还是在 Teleport
的目标位置渲染子组件。
2.3 更新流程
typescript
// update content
n2.el = n1.el
const mainAnchor = (n2.anchor = n1.anchor)!
// 挂载点
const target = (n2.target = n1.target)!
// 锚点
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
// 判断Telport组件是否禁用了
const wasDisabled = isTeleportDisabled(n1.props)
// 如果禁用了,那么挂载点就是周围父组件,否则就是to指定的目标挂载点
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
// 目标挂载点是否是SVG标签元素
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
)
// even in block tree mode we need to make sure all root-level nodes
// in the teleport inherit previous DOM references so that they can
// be moved in future patches.
traverseStaticChildren(n1, n2, true)
} else if (!optimized) {
patchChildren(
n1,
n2,
currentContainer,
currentAnchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
false
)
}
if (disabled) {
if (!wasDisabled) {
// enabled -> disabled
// move into main container
moveTeleport(
n2,
container,
mainAnchor,
internals,
TeleportMoveTypes.TOGGLE
)
} else {
// #7835
// When `teleport` is disabled, `to` may change, making it always old,
// to ensure the correct `to` when enabled
if (n2.props && n1.props && n2.props.to !== n1.props.to) {
n2.props.to = n1.props.to
}
}
} else {
// target changed
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})`
)
}
} else if (wasDisabled) {
// disabled -> enabled
// move into teleport target
moveTeleport(
n2,
target,
targetAnchor,
internals,
TeleportMoveTypes.TOGGLE
)
}
}
}
Teleport
组件更新主要做了以下几件事情:更新子节点、处理 disabled
属性变化、处理 to
属性变化的情况。更新流程的处理步骤拆解如下:
- 更新子节点:可以分为优化更新和普通的全量对比更新两种情况;
- 处理
disabled
属性变化的情况:如果满足新节点disabled
为true
,且旧节点的disabled
为false
的话,说明我们需要把Teleport
的子节点从目标元素内部移回到主视图内部了,此时执行相应的处理逻辑; - 处理
to
属性变化的情况:如果新节点的disabed
为true
,那么先通过to
属性是否改变来判断目标元素target
有没有变化,如果有变化,则把Teleport
的子节点移动到新的target
内部;如果目标元素没变化,则判断旧节点的disabled
是否为true
,如果是则把Teleport
的子节点从主视图内部移动到目标元素内部了。
2.3 卸载流程
当组件卸载的时候会执行 umount
方法,它的内部会判断卸载的组件是否是 Teleport
,如果是则会执行 Teleport
组件的 remove
方法,如下代码所示:
typescript
if (shapeFlag & ShapeFlags.TELEPORT) {
;(vnode.type as typeof TeleportImpl).remove(
vnode,
parentComponent,
parentSuspense,
optimized,
internals,
doRemove
)
}
我们来看看 Teleport
组件 remove
方法的实现:
typescript
// 移除Telport
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 unmount its children whether it's disabled or not
doRemove && hostRemove(anchor!)
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
)
}
}
},
卸载流程中,首先调用 hostRemove
移除主视图渲染的锚点"teleport start"注释节点,然后再去遍历 Teleport
的子节点调用 remove
方法移除子节点。
3、总结
本篇详细解析了 Teleport
组件的使用场景,剖析了创建流程、更新流程以及卸载流程。总体流程如下: