背景
在日常开发中,时常会遇到组件库支持的功能不满足业务场景的需求,在这时就需要开发者自己实现组件或工具方法
问题点
而在开发弹窗组件一类组件时,就会遇到需要我们手动创建组件,并挂在到根目录的问题,下面看看问题怎么解决
解决思路
创建组件
- 在
vue 2.x
中创建组件可以通过h
函数创建组件- 在
vue 3.x
种提供了createVNode
、createElementVNode
等方法用来创建组件
- 创建弹窗容器组件:通过
createElementVNode
创建一个原生DOM元素组件
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
},
);
- 向容器内添加子组件:通过导入其他子组件模板文件,然后通过
createVNode
方法创建组件
//导入子组件
import childrenComponenrt from './components/index.vue'
//...
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
},
createVNode(childrenComponenrt)
);
挂载组件
在vue中,框架提供了两个挂载方法:
mount
方法和render
方法
他们的区别是:
mount
只用于挂在App组件
,也就是应用根组件
,vue 允许一个系统存在多个App实例
,每个实例的上下文是相互独立的render
方法用于将指定VNode节点
挂在到指定父节点下(符合本文需求)
通过上述描述可以发现render
函数更适用于本文,所以如下:
//导入子组件
import childrenComponenrt from './components/index.vue'
//...
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
},
createVNode(childrenComponenrt)
);
render(
containerVNode,
document.querySelector("#app") as Element,
);
销毁组件
在
vue 2.x
中销毁组件通过调用组件的destory()
方法就可以销毁组件
在vue 3.x
中并未提供销毁组件的方法,但是可以通过v-if
指令销毁组件,如下:
- (3.x)通过向外抛出销毁方法,触发子组件的
v-if
指令 值销毁组件
<template>
<div v-if="visible"></div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const visible = ref(true);
defineExpose({
onDestory() {
visible.value = false;
},
});
</script>
<style scoped></style>
//导入子组件
import childrenComponenrt from './components/index.vue'
//...
const childrenVNode=createVNode(childrenComponenrt);
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
onClose=()=>{
childrenVNode.onDestory()
}
},
childrenVNode
);
render(
containerVNode,
document.querySelector("#app") as Element,
);
多层级弹窗
通过上文,可以实现单层弹窗;但如果实现多层级弹窗,上面的例子就不适用了,因为如果还是通过一个变量控制一层弹窗,那么无数层弹窗,岂不是要建无数个变量
解决思路
- 当然也有人会说,可以将组件存进数组啊,那么接下啦我们试验下
//导入子组件
import childrenComponenrt from './components/index.vue'
//...
const childrens:VNode[]=[];
//创建10层弹窗
for (let index = 0; index < 10; index++) {
childrens.push(createVNode(childrenComponenrt));
}
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
onClose=()=>{
const vnode=childrens.pop()
vnode.onDestory()
}
},
childrens
);
render(
containerVNode,
document.querySelector("#app") as Element,
);
通过上面代码可以发现,的确生成了10层弹窗,而且关闭也正常
但是上面的代码仅能执行一次,不能实现响应式、耦合度太高,如果render
以后再添加弹窗,就不会挂载到界面了
- 有人会说,新加弹窗后再
render
一次不就行了吗,如下:
//导入子组件
import childrenComponenrt from './components/index.vue'
//...
const childrens:VNode[]=[];
//初始化创建5层弹窗
for (let index = 0; index < 5; index++) {
childrens.push(createVNode(childrenComponenrt));
}
function mount(){
const containerVNode=createElementVNode(
"div",
{
class: "base-page-dialog",
onClose=()=>{
const vnode=childrens.pop()
vnode.onDestory()
}
},
childrens
);
render(
containerVNode,
document.querySelector("#app") as Element,
);
}
mount();
//挂在后再挂载5层
for (let index = 0; index < 5; index++) {
childrens.push(createVNode(childrenComponenrt));
}
mount()
上面代码执行后会发现,再次挂载的弹窗并未出现,这是因为render不能挂载同样的父节点两次以上,需要先清空挂载,再挂载,如下
//....
//挂在后再挂载5层
for (let index = 0; index < 5; index++) {
childrens.push(createVNode(childrenComponenrt));
}
//清空挂载
render(
null,
document.querySelector("#app") as Element,
);
mount()
- 现在挂载问题解决了,但通过运行上面代码会发现,每次挂载子组件都会被重新渲染,在弹窗的业务场景中并不适用,我们需要的是只关闭顶层节点,下层节点不改变
那么我们分析下哪些因素会触发重新渲染:
- 不能使用
v-if
指令,因为v-if
指令会触发组件重新渲染- 使用
v-show
指令,但h
函数并不能适配v-show
指令,所以只能通过控制目标节点的display
属性伪造v-show
指令render
函数也会触发组件重新渲染,所以在新增弹窗和关闭弹窗后不能调用render
函数
通过上述分析,所以最终只能通过响应式数据,实现动态渲染,才能避免弹窗重新渲染问题,于是改造如下
//导入子组件
import childrenComponenrt from './components/index.vue'
//当前dialog组件栈
const dialogComponents = ref<
{
cp: Component;
vnode?: VNode;
}[]
>([]);
const dialogContainer = ref<VNode>();
function open(cp: Component) {
dialogComponents.value.push({ cp});
if (!dialogContainer.value) {
mount();
}
}
function unmount() {
render(null, document.querySelector("#app") as Element);
dialogContainer.value = undefined;
}
function mount() {
dialogContainer.value = createVNode({
setup() {
return () =>
createElementVNode(
"div",
{
class: "base-page-dialog",
},
dialogComponents.value.map((item, index) => {
if (!toRaw(item).vnode) {
toRaw(item).vnode = createVNode(
item.cp,
{
onClose: () => {
dialogComponents.value.pop();
if (
!dialogComponents.value.length
) {
unmount();
}
},
},
);
} else {
// v-show
toRaw(item).vnode!.el!.style.display =
toRaw(item).vnode!.el!.style.display ==
"none"
? ""
: "none";
}
return item.vnode;
}),
);
},
});
render(
dialogContainer.value,
document.querySelector("#app") as Element,
);
}
//初始化创建5层弹窗
for (let index = 0; index < 5; index++) {
open(childrenComponenrt);
}
//再创建5层弹窗
for (let index = 0; index < 5; index++) {
open(childrenComponenrt);
}
实现原理
在Vue 3.x
中,通过setup
方法返回的组件,将会根据引用到响应式数据动态更新子组件,从而避免了render
重新渲染组件的问题