14.2.2 实现 Teleport 组件
渲染器只做一件事:发现
__isTeleport标识就把控制权交接给 Teleport 自己的process函数;Teleport 内部负责把子节点挂到to指定的容器,并在to变化时把内容搬过去。
一、为什么把渲染逻辑从渲染器中分离出来
- 避免渲染器逻辑代码「膨胀」;
- 可以利用 Tree-Shaking 机制,在最终 bundle 中把 Teleport 相关代码删掉,让构建包体积更小。
实现方式:渲染器只判断识别标识,然后调用组件选项上的 process,并把自己的内部方法作为参数传进去。
} else if (typeof type === 'object' && type.__isTeleport) {
// 组件选项中存在 __isTeleport 标识 → 是 Teleport 组件
// 调用它的 process 函数,把控制权交接出去
// 第五个参数是渲染器的一些内部方法
type.process(n1, n2, container, anchor, {
patch,
patchChildren,
unmount,
move(vnode, container, anchor) {
insert(
vnode.component
? vnode.component.subTree.el // 移动一个组件
: vnode.el, // 移动普通元素
container,
anchor
)
}
})
}
分组原则:通用能力(patch / patchChildren / unmount / move)由渲染器提供,Teleport 特有的位置逻辑写在 Teleport 自己身上。
二、Teleport 组件实现
const Teleport = {
__isTeleport: true,
process(n1, n2, container, anchor, internals) {
// 通过 internals 取得渲染器的内部方法
const { patch, patchChildren, move } = internals
if (!n1) {
// 挂载
// 获取容器(挂载点):字符串当选择器,否则直接当容器元素
const target =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to
// 把 n2.children 渲染到指定挂载点(注意:不是 container,而是 target)
n2.children.forEach(c => patch(null, c, target, anchor))
} else {
// 更新
// 1. 更新子节点
patchChildren(n1, n2, container)
// 2. to 变化时,内容需要换容器
if (n2.props.to !== n1.props.to) {
const newTarget =
typeof n2.props.to === 'string'
? document.querySelector(n2.props.to)
: n2.props.to
// 把子节点逐个移动到新容器
n2.children.forEach(c => move(c, newTarget))
}
}
}
}
要点:
| 环节 | 处理方式 |
|---|---|
| 渲染位置 | 挂载时用 n2.props.to 解析出 target,children 挂到 target 而不是渲染器给的 container |
to 的取值 | 字符串视为 CSS 选择器(document.querySelector),否则视为容器元素 |
| 子节点类型 | Teleport 的子节点编译为普通数组,不是插槽对象,直接 forEach 即可 |
| 更新 | 先 patchChildren 更新内容,再比较 n2.props.to !== n1.props.to,不同则移动 |
| 移动实现 | move 内部区分组件(vnode.component.subTree.el)与普通元素(vnode.el) |
三、测试用例
const trigger = ref(false)
const CompVNode = {
type: Teleport,
// 以普通 children 的形式代表被 Teleport 的内容
props: { to: 'body' },
children: [
{ type: 'h1', children: 'Title' },
{ type: 'p', children: 'pppp' }
]
}
setTimeout(() => {
trigger.value = !trigger.value
}, 2000)
effect(() => {
renderer.render(
trigger.value
? { ...CompVNode, props: { to: 'body' } }
: { ...CompVNode, props: { to: '#app' } },
document.querySelector('#app')
)
})
两秒后切换 to,内容会从 body 整体搬到 #app,正是 process 里那段「比较 target 变化 + move」。
四、实现时容易漏掉的点
move用insertBefore实现:DOM 节点被插入另一个父节点时会自动先从原位置移除,所以「搬家」天然满足「先移除再挂载」,不需要手写removeChild。
Teleport 的内容被搬走后,原位置需要一个占位节点记录插入位置,否则后续插入兄弟节点时找不到锚点(官方实现里用占位节点 / targetAnchor 处理,本例做了简化)。
官方 Teleport 还提供
disabled属性:为true时不做传送,内容原地渲染(相当于临时关掉 Teleport),常用于在响应式切换中避免无谓的搬移;它与to的变化一样,要求实现能正确地在「原地」与「目标容器」之间搬运内容。
to传字符串时会执行document.querySelector,只应在客户端真实 DOM 环境使用;to指向不存在的元素会得到null,导致插入报错,切换to时务必确认目标已存在。
相关
- 14.2.1 Teleport 组件要解决的问题:为什么需要跨 DOM 层级渲染
- 14.1.1 组件的激活与失活:另一种「把内容搬到别的容器」的内建组件实现思路