普通视图

发现新文章,点击刷新页面。
今天 — 2026年1月28日首页

老项目 Vue2 函数式打开弹层【附源码】

2026年1月28日 13:59

大家好,我是前端架构师,关注微信公众号【@程序员大卫】免费领取前端精品资料。

前言

在老项目(Vue2 + ElementUI)里写弹层,大家一定很熟悉下面这种写法:

<dialog-test
  v-if="dialogVisible"
  :text="text"
  @confirm="handleConfirm"
  @close="dialogVisible = false"
/>

问题是:

  • 每个页面都要写一堆 visible
  • 关闭弹层还得手动销毁组件
  • 多个弹层时,代码又臭又长
  • 只是想 “点个按钮弹个框”,却要改一堆模板

所以我在老项目里封装了一个 函数式打开弹层 的方法:

  • 不用写 template,不用定义变量 dialogVisible
  • 直接 JS 调用
  • 自动创建 / 销毁
  • 支持缓存,不销毁反复用

最终效果

像这样直接调用即可:

dialogOpen(
  DialogTest,
  {
    text: "Hello World!",
    onConfirm: (text) => {
      console.log(text);
    },
  },
  { context: this },
);

一句话总结:

把“写组件”这件事,变成“调函数”

一、弹层组件本身(DialogTest.vue)

这个组件本身非常普通,没有任何黑魔法。

<template>
  <el-dialog title="提示" visible width="30%" @close="doClose" append-to-body>
    <span>{{ text }}</span>

    <span slot="footer" class="dialog-footer">
      <el-button type="primary" @click="doCancle">取消</el-button>
      <el-button type="primary" @click="doConfirm">确 定</el-button>
    </span>
  </el-dialog>
</template>

关键点说明

  • visible 由外部控制(函数式传入)
  • 所有行为通过 $emit 往外抛
export default {
  name: "DialogTest", // ⚠️ 必须有 name,后面做缓存用
  props: {
    text: String,
  },
  methods: {
    doClose() {
      this.$emit("close");
    },
    doCancle() {
      this.doClose();
    },
    doConfirm() {
      this.$emit("confirm", "点击确认按钮");
      this.doClose();
    },
  },
};

记住一句话:组件只负责展示和抛事件,不管怎么被创建。

二、核心思路:函数式弹层是怎么实现的?

一句话概括实现原理:

new Vue() 在 JS 里动态创建组件,并手动挂载到 body 上

也就是说,我们不再依赖 template,而是:

  1. JS 创建 Vue 实例
  2. render 一个 Dialog 组件
  3. 挂载到 DOM
  4. 关闭时销毁或隐藏

三、dialogOpen.js 整体结构说明

我们先看函数签名:

dialogOpen(Component, props = {}, options = {})

三个参数分别是:

参数 说明
Component 弹层组件
props 传给组件的 props + 事件
options.context 当前页面的 this
options.destroyOnClose 关闭是否销毁(默认 true)
options.key 缓存 key

四、为什么必须传 context?

dialogOpen(DialogTest, {...}, { context: this })

这是很多人第一次看不懂的地方。

原因只有一个:

让弹层组件继承当前页面的 $router$store 等上下文

new Vue({
  parent: context
})

否则:

  • $router 可能是 undefined
  • $store 用不了
  • inject / provide 失效

五、props 和事件是如何区分的?

调用时我们这样写:

dialogOpen(DialogTest, {
  text: "Hello World",
  onConfirm: () => {},
  onClose: () => {}
})

那问题来了:

  • 哪些是 props?
  • 哪些是事件?

解决方案:统一约定

onXxx => 事件
其它 => props

对应代码:

const splitPropsAndListeners = (input = {}) => {
  const props = {};
  const on = {};

  Object.keys(input).forEach(key => {
    if (/^on[A-Z]/.test(key) && typeof input[key] === 'function') {
      const event = key.slice(2).replace(/^[A-Z]/, s => s.toLowerCase());
      on[event] = input[key];
    } else {
      props[key] = input[key];
    }
  });

  return { props, on };
};

最终会变成:

h(Component, {
  props: { text },
  on: { confirm, close }
})

六、真正创建弹层的地方(核心代码)

let wrapperVm = new Vue({
  parent: context,
  data() {
    return {
      rawProps: initialProps
    };
  },
  computed: {
    vnodeData() {
      const { props, on } = splitPropsAndListeners(this.rawProps);
      return { props, on };
    }
  },
  render(h) {
    return h(Component, {
      props: this.vnodeData.props,
      on: {
        ...this.vnodeData.on,
        close: (...args) => {
          this.vnodeData.on.close?.(...args);
          closeHandler();
        }
      }
    });
  }
});

这里发生了什么?

  1. rawProps 保存所有传入参数
  2. computed 动态拆分 props / 事件
  3. render 手动渲染组件
  4. 拦截 close,统一处理销毁逻辑

七、弹层是怎么挂载到页面上的?

let container = document.createElement('div');
document.body.appendChild(container);
wrapperVm.$mount(container);

👉 这一步相当于:

<body>
  <div>
    <!-- Dialog 组件 -->
  </div>
</body>

八、关闭时销毁 vs 不销毁(缓存机制)

默认行为(destroyOnClose = true)

wrapperVm.$destroy();
removeElement(wrapperVm.$el);

好处:

  • 不占内存
  • 最安全

开启缓存(destroyOnClose = false)

const instanceCache = new Map();
  • 同一个 key 只创建一次
  • 关闭时只是 visible = false
  • 再次打开直接复用
if (!destroyOnClose && instanceCache.has(key)) {
  const cacheWrapperVm = instanceCache.get(key);
  cacheWrapperVm.updateProps(initialProps);
  return cacheWrapperVm.triggerClose;
}

适合:

  • 表单弹层
  • 频繁打开的弹窗

九、为什么要监听页面销毁?

context.$once('hook:beforeDestroy', destroy);

防止:

  • 页面销毁了
  • 弹层还留在 body
  • 造成内存泄漏

十、总结

这个方案适合:

  • Vue2 老项目
  • ElementUI 弹层
  • 不想每个页面都写 dialog

附源码

github.com/zm8/wechat-…

❌
❌