Element UI MessageBox 渲染虚拟节点的坑与解决方案
目录
1. 背景
2. 现象与问题
3. 前置知识:MessageBox 渲染逻辑
4. 尝试过的方案
方案 A:强制刷新
方案 B:给 VNode 加 key
5. 最终方案 —— 提炼成独立组件
6. 为什么提炼组件就能解决?
8. ✍️ 结尾总结
1. 背景
在业务开发中,我们经常需要用 Element UI 的 MessageBox(this.$msgbox
) 来做交互确认。例如:
-
输入框(用
$prompt
) -
简单确认(用
$confirm
) -
信息展示(用
$alert
)
但有时候需求比较复杂,比如要在弹窗里放一个 下拉选择器、单选框组、甚至一个完整的表单。此时我们一般会用:
const h = this.$createElement
this.$msgbox({title: '选择票种',message: h('el-select', { ... }),...
})
这其实就是在 message
里传入一个 VNode(虚拟节点),而不是纯字符串。
2. 现象与问题
直接把 <el-select>
或 <el-radio-group>
这样传进去后会发现:
-
数据更新了,但 UI 不实时刷新。
-
关闭再打开 MessageBox,才会看到上一次的选择结果。
这就是开发中常见的“选了不显示,显示是旧的”的现象。
最开始可能会写成这样:
const h = this.$createElement
await this.$msgbox({title: '提示',message: h('el-select', {props: { value: this.tempInvoiceType },on: { input: val => { this.tempInvoiceType = val } }}, [h('el-option', { props: { label: 'bs', value: 'bs' } }),h('el-option', { props: { label: '全电专票', value: '全电专票' } }),h('el-option', { props: { label: 'pc', value: 'pc' } }),h('el-option', { props: { label: '全电普票', value: '全电普票' } })]),showCancelButton: true,confirmButtonText: '确定',cancelButtonText: '取消'
})
问题就是:选择后值有了,但 UI 不更新。
3. 前置知识:MessageBox 渲染逻辑
MessageBox 的参数中,最关键的几个是:
this.$msgbox({title: '发票类型',message: h(...), // 可以是字符串,也可以是 VNodeshowCancelButton: true, // 是否显示取消按钮confirmButtonText: '确认', // 确认按钮文字cancelButtonText: '取消', // 取消按钮文字closeOnClickModal: false, // 是否点击遮罩关闭closeOnPressEscape: false, // 是否按 ESC 关闭beforeClose: (action, instance, done) => {// 确认/取消/关闭时的拦截}
})
message
参数
-
如果是字符串,MessageBox 内部直接插进去。
-
如果是 VNode(通过
h()
创建),它会在 MessageBox 的 Vue 实例中渲染一次。
⚠️ 这里的重点:VNode 是“一次性的快照”。
它不会自动和你外层组件的响应式系统保持同步。
4. 尝试过的方案
方案 A:强制刷新
在 input
事件里调用 this.$forceUpdate()
,让外层组件强制更新。
👉 问题:不生效,MessageBox 内部未必能 patch 正确。
方案 B:给 VNode 加 key
每次打开时给 el-select
加一个随机 key
,迫使 Vue 重新挂载。
👉 问题:只能解决“打开时显示上次选择”的问题,无法解决“实时更新 UI”的问题。
5. 最终方案 —— 提炼成独立组件
真正稳定的做法,是不要直接在 MessageBox 里写 VNode,而是提炼成一个独立的 Vue 组件,然后作为 message
传进去。
例如:
import Vue from 'vue'const CustomRadioGroup = Vue.component('CustomRadioGroup', {props: ['value'],data() {return {internalValue: this.value}},watch: {value(newVal) {this.internalValue = newVal}},render(h) {return h('el-radio-group',{props: { value: this.internalValue },on: {input: value => {this.internalValue = valuethis.$emit('input', value)}}},[h('el-radio', { props: { label: '1' }}, '轻微瑕疵品'),h('el-radio', { props: { label: '2' }}, '中度瑕疵品')])}
})export default CustomRadioGroup
在调用处:
import CustomRadioGroup from './CustomRadioGroup'const h = this.$createElement
await this.$msgbox({title: '发票类型',message: h(CustomRadioGroup, {props: { value: this.invoiceLine },on: {input: value => { this.invoiceLine = value }}}),showCancelButton: true,confirmButtonText: '确认',cancelButtonText: '取消',closeOnClickModal: false,closeOnPressEscape: false,beforeClose: (action, instance, done) => {if (action === 'confirm') {if (!this.invoiceLine) {this.$message.error('请选择发票类型')return}done()} else done()}
})console.log('选择的发票类型:', this.invoiceLine)
6. 为什么提炼组件就能解决?
-
自定义组件本质上是一个独立的小 Vue 实例,它有自己的响应式系统。
-
MessageBox 拿到的只是这个组件的挂载点,当父组件的数据(
invoiceLine
)变化时,自定义组件会自动响应并更新视图。 -
这样就避免了 VNode 一次性快照的问题。
换句话说:
👉 VNode 直接传进去 = 死的快照
👉 封装成组件传进去 = 活的实例
8. ✍️ 结尾总结
-
简单提示 → 用字符串
message
。 -
简单输入 → 用
$prompt
。 -
复杂交互(选择器、单选、多选、表单) → 一定要提炼成独立组件,再放到 MessageBox 里。
这样不仅解决了 UI 不刷新的问题,也让代码更清晰、更可维护。
在 Element UI 的 MessageBox 里,如果你要渲染复杂交互组件,记住一句话:
不要直接传 VNode,要传组件。
这个问题在 Element 官方 issue #8931 中也有讨论。