前端vue3中useTeleport实现,再也用不写模板了

425 阅读4分钟

背景

哈哈 我是 会爬树的金鱼,树上的金鱼呦
需求开发中部分场景会用到 teleport 标签,将内容挂在在其他地方展示如:弹窗 需要模版中添加这一段代码:

 <teleport to="selector">
  ...要展示的内容
 </teleport>

显示的内容与当前组件的模版耦合在一起,还要自己维护显示状态,使用上很不方便,增加了维护量,对于我这种有代码洁癖的初级开发来说是不能忍受的,有没有不需要添加这段代码的方式呢?

答案是有的 要不然组件库那些弹窗命令式是怎么来的呢?

直接上代码

要实现脱离模版就只能直接操作VNode了,要实现命令式分3个步骤

  1. 创建VNode 这步就靠 createVNode 函数了,也可以用 h 函数 但我这上了ts用 h 函数的入参类型对于我这初级前端来说真不好搞比较麻烦 createVNode 函数的类型就比较简单搞定了,这里有个坑,创建出的 VNode 就如新出生的宝宝,纯净如白纸,不带当前 Vue 实例的上下文导致全局注册的组件和指令无法使用,需要给他添加上才能正常使用。
import { createVNode } from 'vue';

const newCmp = createVNode(cmp);
// 将给予当前实例上的上下文
newCmp.appContext = app.appContext;


  1. 挂在到指定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()
})

完结撒花!