背景
哈哈 我是 会爬树的金鱼,树上的金鱼呦
需求开发中部分场景会用到 teleport 标签,将内容挂在在其他地方展示如:弹窗
需要模版中添加这一段代码:
<teleport to="selector">
...要展示的内容
</teleport>
显示的内容与当前组件的模版耦合在一起,还要自己维护显示状态,使用上很不方便,增加了维护量,对于我这种有代码洁癖的初级开发来说是不能忍受的,有没有不需要添加这段代码的方式呢?
答案是有的 要不然组件库那些弹窗命令式是怎么来的呢?
直接上代码
要实现脱离模版就只能直接操作VNode了,要实现命令式分3个步骤
- 创建VNode 这步就靠 createVNode 函数了,也可以用 h 函数 但我这上了ts用 h 函数的入参类型对于我这初级前端来说真不好搞比较麻烦 createVNode 函数的类型就比较简单搞定了,这里有个坑,创建出的 VNode 就如新出生的宝宝,纯净如白纸,不带当前 Vue 实例的上下文导致全局注册的组件和指令无法使用,需要给他添加上才能正常使用。
import { createVNode } from 'vue';
const newCmp = createVNode(cmp);
// 将给予当前实例上的上下文
newCmp.appContext = app.appContext;
- 挂在到指定DOM里 查阅资料后发现 可以用 render 函数实现
render函数可以接收两个参数 vnode 和 container ,要渲染的节点和渲染的容器 container 必须是个 Element 可以通过 document.querySelector 方法获取
import { render } from 'vue';
render(vnode, container);
哈哈,怎么样很简单吧,核心就两个函数,完成了挂载操作 3. 卸载节点 使用上没啥创建 VNode 挂载到 DOM 里后 DOM 销毁了 VNode 不会跟随销毁,这可能导致内存泄漏,既然渲染是通过 render 函数,解铃还需系铃人 这还得要 render 函数出马了 只需要将第一个参数为空即可,render 函数会把与 container 绑定的 VNode 解绑释放掉。
import { render } from 'vue';
render(null, container)
封装
原理部分完成了,现在开始封装,先个挂载方法设计使用方法,为了方便设计为两个参数,第一个为对应组件,第二个为挂载点,
/**
* 传送门--将VNode实例化并且渲染到指定节点里面,注意: 需要手动销毁或在onBeforeUnmount里销毁
* @param vnode 虚拟节点
* @param selectors 选择器或DOM
* @returns destroy 用于销毁当前组件的方法,销毁后vnode可以再次使用
*/
export const teleport = (vnode: VNode, selectors: string | Element) => {
let container: Element | null;
// 清空函数
const destroy = () => {
// 清空节点,会触发绑定当前DOM的组件生命周期,解除绑定
container && render(null, container);
// 从vnode上清空el属性,为识别已清空
vnode.el = null;
};
// 如果是字串则认为是选择器
if (typeof selectors === 'string') {
container = document.querySelector(selectors);
} else {
container = selectors as Element;
}
if (!container) {
throw new Error(`选择器${selectors}未找到对应节点请确认当前时机已被渲染`);
}
// 清空当前DOM,让已绑定此DOM的组件触发生命周期销毁掉
render(null, container);
// 将vnode绑定DOM
render(vnode, container);
return destroy;
};
入参是个 VNode 这里还将组件用 createVNode 创建实例为VNode才能使用,需要还需要一层
/**
* 创建一个组件
* 注意: 需要手动销毁或在onBeforeUnmount里销毁
* @param cmp 组件对象或字串模板
* @param container 选择器或DOM
* @returns \{ component, teleport, destroy }
*/
export const useTeleport = (cmp: Parameters<typeof createVNode>[0]) => {
const app = getCurrentInstance();
if (!app) {
// 必须在 setup 函数内执行否则 getCurrentInstance 获取不到当前实例
return;
}
const newCmp = createVNode(cmp);
newCmp.appContext = app.appContext;
// 销毁回调
let destroyCallBack: ReturnType<typeof teleport> | null = null;
// 销毁函数
const destroy = () => destroyCallBack && destroyCallBack()
return {
// 创建好的组件实例
component: newCmp.component,
// 在需要的地方调用这个方法进行传送
teleport: (container: string | Element) => {
destroyCallBack = teleport(newCmp, container)
return destroyCallBack
},
destroy
};
};
这样就基本完成了 使用方法
const { teleport, destroy } = useTeleport(cmp)
// 按钮回调
const handleCallBack = ()=>{
// 挂载到指定DOM内, 可以是 字串 或 Element
teleport(DOMRef.value)
// 用完后可以调用destroy方法销毁掉
}
// 当前节点被销毁时记得将外面的兄弟带回
onBeforeUnmount(()=>{
destroy()
})
完结撒花!