老项目 Vue2 函数式打开弹层【附源码】
大家好,我是前端架构师,关注微信公众号【@程序员大卫】免费领取前端精品资料。
前言
在老项目(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,而是:
- JS 创建 Vue 实例
- render 一个 Dialog 组件
- 挂载到 DOM
- 关闭时销毁或隐藏
三、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();
}
}
});
}
});
这里发生了什么?
-
rawProps保存所有传入参数 -
computed动态拆分 props / 事件 -
render手动渲染组件 - 拦截
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