【解决方案】Vue 3 如何手动挂载元素到指定节点

发布于:2024-12-08 ⋅ 阅读:(139) ⋅ 点赞:(0)

背景

在日常开发中,时常会遇到组件库支持的功能不满足业务场景的需求,在这时就需要开发者自己实现组件或工具方法

问题点

而在开发弹窗组件一类组件时,就会遇到需要我们手动创建组件,并挂在到根目录的问题,下面看看问题怎么解决

解决思路

创建组件
  • vue 2.x中创建组件可以通过h函数创建组件
  • vue 3.x种提供了createVNodecreateElementVNode 等方法用来创建组件
  1. 创建弹窗容器组件:通过createElementVNode创建一个原生DOM元素组件
const containerVNode=createElementVNode(
	"div",
	{
	    class: "base-page-dialog",
	},
);
  1. 向容器内添加子组件:通过导入其他子组件模板文件,然后通过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指令销毁组件,如下:

  1. (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,
);

多层级弹窗

通过上文,可以实现单层弹窗;但如果实现多层级弹窗,上面的例子就不适用了,因为如果还是通过一个变量控制一层弹窗,那么无数层弹窗,岂不是要建无数个变量

解决思路
  1. 当然也有人会说,可以将组件存进数组啊,那么接下啦我们试验下
//导入子组件
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以后再添加弹窗,就不会挂载到界面了

  1. 有人会说,新加弹窗后再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()
  1. 现在挂载问题解决了,但通过运行上面代码会发现,每次挂载子组件都会被重新渲染,在弹窗的业务场景中并不适用,我们需要的是只关闭顶层节点,下层节点不改变

那么我们分析下哪些因素会触发重新渲染:

  1. 不能使用v-if指令,因为v-if指令会触发组件重新渲染
  2. 使用v-show指令,但h函数并不能适配v-show指令,所以只能通过控制目标节点的display属性伪造v-show指令
  3. 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重新渲染组件的问题


网站公告

今日签到

点亮在社区的每一天
去签到