vue2和vue3函数式调用组件学习记录
🚀 对比说明
功能 | Vue 2 版本 | Vue 3 改写 |
---|---|---|
动态创建 | Vue.extend + $mount() | createVNode + render() |
控制显示 | data.show | ref(show) + v-model:show |
销毁组件 | $destroy() | render(null, container) |
暴露方法 | 直接访问实例 | defineExpose() |
语法风格 | Options API | <script setup> + Composition API |
一、vue2
使用 Vue.extend
将定义对象转为一个可实例化的构造函数,然后挂载到一个独立 DOM 节点。
我们以一个假期弹窗作为示例
<template><van-popup v-model="show" @close="$emit('close')" :close-on-popstate="true" :close-on-click-overlay="true"><div class="popup-box"><div class="title">温馨提示</div><div class="popup-text" v-html="content"></div><van-button round block color="linear-gradient(to right, #F0D19A, #DEAA62)" @click="$emit('close')">{{ buttonText || '知道了'}}</van-button></div></van-popup>
</template>
<script>
import Vue from 'vue'
const HolidayPopup = {name: 'HolidayPopup',props: {content: String,buttonText: String},data() {return {show: false}},methods: {close() {this.show = false},onConfirm() {this.$emit('confirm', true)}}
}
export default HolidayPopupexport const holidayInst = function (props) {return new Promise(resolve => {const constructor = Vue.extend(HolidayPopup)const inst = new constructor({ propsData: props }).$mount()inst.$on('confirm', val => {inst.close()resolve(val)})inst.$on('close', () => {setTimeout(() => {inst.$destroy()document.body.removeChild(inst.$el)resolve(true)}, 300)})document.body.appendChild(inst.$el)inst.show = true})
}
</script><style scoped lang="scss">
.....省略</style>
使用方式
holidayInst({content: '放假通知:10月1日至10月7日休假!',buttonText: '我知道了'
}).then(() => {console.log('用户关闭了弹窗')
})
二、vue3
这是函数式调用封装(Vue 3 推荐使用 createVNode
+ render
)
HolidayPopup.vue
<template><van-popupv-model:show="show"@close="handleClose"close-on-popstateclose-on-click-overlay><div class="popup-box"><div class="title">温馨提示</div><div class="popup-text" v-html="content"></div><van-buttonroundblockcolor="linear-gradient(to right, #F0D19A, #DEAA62)"@click="handleClose">{{ buttonText || '知道了' }}</van-button></div></van-popup>
</template><script setup>
import { ref, defineExpose } from 'vue'const props = defineProps({content: String,buttonText: String
})const show = ref(false)const emit = defineEmits(['confirm', 'close'])const handleClose = () => {show.value = falseemit('close')
}const open = () => {show.value = true
}defineExpose({open,close: handleClose
})
</script><style scoped lang="scss">
......省略
</style>
holidayInst.js
import { createVNode, render } from 'vue'
import HolidayPopup from './HolidayPopup.vue'export function holidayInst(props) {return new Promise((resolve) => {const container = document.createElement('div')document.body.appendChild(container)const vnode = createVNode(HolidayPopup, {...props,onClose: () => {// 弹窗关闭时销毁实例render(null, container)document.body.removeChild(container)resolve(true)}})render(vnode, container)// 等待组件实例挂载后再显示const inst = vnode.component?.exposedif (inst && inst.open) {inst.open()}})
}
使用方式:
import { holidayInst } from '@/components/HolidayPopup/holidayInst'async function showHolidayNotice() {await holidayInst({content: '放假通知:10月1日至10月7日放假,祝大家假期愉快!',buttonText: '我知道了'})console.log('弹窗已关闭')
}
🟢 三、适用场景(什么时候适合这种调用方式)
这种组件调用方式适合「临时出现 / 全局生效 / 与组件树无强关联」的交互场景。
✅ 常见使用场景:
场景 | 示例 | 原因 |
---|---|---|
1️⃣ 全局弹窗 / 消息提示 | Toast.success('保存成功') | 轻量、无需模板,逻辑触发后直接弹出 |
2️⃣ 确认框、警告框 | Dialog.confirm({ message: '确定删除?' }) | 操作前确认提示,不依赖父组件 |
3️⃣ 系统公告 / 节日弹窗 | holidayInst({ content: '国庆放假通知' }) | 在 App 启动或全局逻辑里触发 |
4️⃣ 登录过期提示 | 全局拦截器里调用弹窗 | 无法在模板里声明,只能用函数创建 |
5️⃣ 图片预览 / 全屏展示类组件 | ImagePreview.open(list) | 全局层级高,独立于业务组件树 |
⚙️ 四、使用这种方式的优势
优点 | 说明 |
---|---|
✅ 调用简单 | 不需要在模板中声明 <Dialog /> ,直接一行代码调用 |
✅ 全局可用 | 可以在任意地方调用,比如接口拦截器、store、router 等 |
✅ 使用体验好 | 像调用函数一样方便,搭配 Promise 异步处理结果很自然 |
✅ 无状态干扰 | 不依赖父组件的数据或生命周期,逻辑隔离清晰 |
✅ 动态数量灵活 | 可以随时创建多个实例(比如多个 Toast) |
🔴 五、潜在弊端(使用时要注意的坑)
弊端 | 说明 | 对策 |
---|---|---|
⚠️ 不受 Vue 组件树管理 | 不在正常的组件层级中,Vue 不会自动销毁 | 手动执行 render(null) / $destroy() 清理 |
⚠️ 可能造成内存泄漏 | 多次创建实例但未销毁,会堆积在内存中 | 调用完后一定要清理 DOM 和实例 |
⚠️ 难以调试 / 跟踪 | 组件不在模板里,不容易定位 | 给组件添加唯一标识或日志 |
⚠️ 状态不可共享 | 与其他组件的 reactive 状态不共享 | 仅用于“独立展示型组件” |
⚠️ SSR 不兼容 | 涉及 document.body 操作,无法在服务端渲染执行 | SSR 环境中需禁用或判断执行环境 |
⚠️ 动画/过渡控制复杂 | 因为是动态挂载,过渡钩子要自己处理 | 可监听 @closed 后再销毁 |
💎 函数式调用组件(Programmatic Component)的优势
也就是像这样用的组件:
holidayInst({ content: '放假通知' })
Toast.success('保存成功')
Dialog.confirm({ message: '确认删除?' })
这类组件不写在模板里,而是直接用代码弹出。
它的优势主要体现在 开发灵活性、调用便捷性、全局可用性 三个方面 👇
🟢 一、调用更灵活(脱离模板约束)
不需要在
<template>
中声明组件。可以在任意 JS 逻辑中调用,比如:
Vuex / Pinia 的 action 中;
Axios 拦截器里;
路由守卫(
router.beforeEach
);甚至纯 JS 模块(无 Vue 上下文)。
✅ 示例:
axios.interceptors.response.use(res => res,err => {if (err.response.status === 401) {Dialog.alert({ message: '登录已过期,请重新登录' })}return Promise.reject(err)}
)
普通组件做不到,因为它必须依附在模板或页面中。
⚡ 二、使用方式简单直观
调用形式类似“工具函数”,逻辑清晰;
无需维护
v-if
、v-show
状态;通过 Promise 直接拿到用户行为结果。
✅ 示例:
Dialog.confirm({ message: '确定删除?' }).then(() => deleteItem()).catch(() => console.log('取消'))
相比:
<MyDialog v-model="visible" @confirm="deleteItem" />
→ 不需要手动管理 visible
,逻辑更纯粹。
🌍 三、可在全局任意地方调用
因为它是动态挂载到
document.body
的;所以不依赖父组件或上下文;
常用于 全局统一提示 / 系统级弹窗。
✅ 常见应用:
登录过期提示
全局公告弹窗
Loading、Toast、Notify
图片预览、全屏播放器
🧱 四、实例隔离、互不干扰
每次调用都会创建新的组件实例,不会污染其他页面的状态。
适合:
多个同时存在的 Toast;
并发消息提示;
独立逻辑的确认框。
🎨 五、便于封装统一的 UI 行为
可以把 UI 弹窗逻辑和业务逻辑完全分离,
团队中可统一封装如:
// useDialog.js
export const useDialog = (message) => { return Dialog.confirm({ message, title: '系统提示'
}) }
这样业务方只需调用:
await useDialog('确定删除?')
无需关心组件实现,方便维护和替换。
🚀 六、减少模板污染 & 提升可维护性
声明式组件:
<MyDialog v-model="showDialog" :title="title" @confirm="confirmFn" />
函数式调用:
MyDialog({ title, message }).then(confirmFn)
模板更干净;
状态逻辑转为函数逻辑;
对复用组件库开发非常友好。
✅ 总结对比表
优势 | 说明 |
---|---|
💬 使用简单 | 像函数一样调用,无需 v-if/v-show |
🌍 全局可用 | 任意位置可调用(即使无 Vue 上下文) |
🧩 逻辑解耦 | UI 与业务逻辑分离 |
🧱 实例独立 | 每次调用独立创建,不干扰其他实例 |
🎨 模板干净 | 不占用模板结构,适合全局弹窗类组件 |
⚙️ Promise 接口自然 | 可以方便地使用异步/等待用户操作 |
✅ 一句话总结:
函数式调用组件最大的优势是——
不受模板限制、调用灵活、使用简单,非常适合「全局弹出类 UI」场景。