前言
Teleport
是Vue3中的内建组件,它能够将它的children挂载到指定的目标节点上。通常在写modal
dropdown
这类组件时需要用到。而在Vue2并没有内置这个组件,本文我们就来探讨一下如何在Vue2中实现相同的功能。
思考
上面提到 Teleport
组件是将它的children挂载到指定的目标节点上。而正常情况下,元素挂载在哪里取决于你将它写在哪里,那么我们要怎么才能实现跨越层级的挂载呢?比较容易想到的就是 new Vue
这样的方式了,下面我们就以这种方式去实现一个 Teleport
组件。
实现
import Vue, { defineComponent, onMounted, onUnmounted, onUpdated } from 'vue';
import type { PropType, VueConstructor } from 'vue'
export const Teleport = defineComponent({
props: {
// 定义 to 属性以供用户可以指定挂载到哪个节点上
to: {
type: [String, Object] as PropType<string | HTMLElement>,
default: () => document.body,
}
},
setup(props, { slots }) {
// 获取挂载目标节点
const container = typeof props.to === 'string'
? document.querySelector(props.to)
: props.to
let div: HTMLDivElement | null = document.createElement('div')
container?.appendChild(div)
let vm: InstanceType<VueConstructor> | null = null;
onMounted(() => {
if (div) {
// 跨层挂载的核心
vm = new Vue({
el: div,
render: () => {
// 只能挂载第一个元素,如需挂载多个可以使用div包裹一层,或者使用 vue-fragment
return slots.default?.()[0]
}
})
}
})
onUnmounted(() => {
if (vm) {
vm.$destroy()
div && document.body.removeChild(div)
div = null
vm = null
}
})
onUpdated(() => {
vm?.$forceUpdate();
})
return () => null
}
})
如上代码所示,首先我们需要定义一个props to
让用户可以自定义挂载到哪个节点上,默认挂载到 document.body
。然后我们需要创建一个 div
并append到挂载目标元素上,接着通过 new Vue
的方式将需要被挂载的元素挂载到 div
上,这样就大致实现了跨层挂载组件了。但仅仅这样是不够的,别忘了还需要处理 onUpdated
onUnmounted
的情况。
又一个问题
如上所示大致实现了这样一个组件,但是我在使用中又遇到一个问题,请看下面代码:
export const Modal = defineComponent({
setup() {
return () => h(Teleport, [
h('div', { class: 'modal' })
])
}
})
以上代码看起来似乎没问题,可当你使用 Modal 组件并想为其容器加一些class或者style样式时,就会发现没有效果。原因是组件的根元素不是一个具体的元素了,取而代之的是 Teleport
组件,而vue只会将父元素传递的class和style属性集成给组件的根节点。因此目前这种情况是无法接收到外部传入的class与style的。
而我在尝试了 attrs
和 props
上查找class和style属性,发现都无法获取到。最终通过 getCurrentInstance
找到了办法,请看下面代码:
export const Modal = defineComponent({
setup() {
const vm = getCurrentInstance()?.proxy;
return () => h(Teleport, [
h('div', {
staticClass: vm?.$vnode.data?.staticClass ? `modal ${vm.$vnode.data.staticClass}` : 'modal',
staticStyle: {
...(vm?.$vnode.data?.staticStyle || {}),
},
style: vm?.$vnode.data?.style,
class: vm?.$vnode.data?.class
})
])
}
})
这样就完美解决了无法继承传进来的class和style了。
结语
该组件的核心逻辑并不难,难得是处理边角情况和优化。目前这并不是完美版的,比如没有去考虑to属性动态更新的情况,需要销毁旧的Vue实例再重新创建一个实例。但在Vue3中并不是这么实现的,由于在Vue3中是内建组件,有渲染器的加持,当 to
发生变化时仅需将节点移动至新的目标节点即可,这样做极大的节省了性能消耗。但在Vue2中似乎很难做到这一点。不过这类需求毕竟是少数的,以目前的版本也足以应对大多数场景了。