详细介绍: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 节点。
我们以一个假期弹窗作为示例
温馨提示
{{ buttonText || '知道了'}}
<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 HolidayPopup
export 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>
使用方式
holidayInst({
content: '放假通知:10月1日至10月7日休假!',
buttonText: '我知道了'
}).then(() => {
console.log('用户关闭了弹窗')
})
二、vue3
这是函数式调用封装(Vue 3 推荐使用 createVNode + render)
HolidayPopup.vue
温馨提示
{{ buttonText || '知道了' }}
<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 = false
emit('close')
}
const open = () => {
show.value = true
}
defineExpose({
open,
close: handleClose
})
</script>
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?.exposed
if (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('取消'))
相比:
→ 不需要手动管理 visible,逻辑更纯粹。
三、可在全局任意地方调用
因为它是动态挂载到
document.body的;所以不依赖父组件或上下文;
常用于 全局统一提示 / 系统级弹窗。
✅ 常见应用:
登录过期提示
全局公告弹窗
Loading、Toast、Notify
图片预览、全屏播放器
四、实例隔离、互不干扰
每次调用都会创建新的组件实例,不会污染其他页面的状态。
适合:
多个同时存在的 Toast;
并发消息提示;
独立逻辑的确认框。
五、便于封装统一的 UI 行为
可以把 UI 弹窗逻辑和业务逻辑完全分离,
团队中可统一封装如:
// useDialog.js
export const useDialog = (message) => {
return Dialog.confirm({ message, title: '系统提示'
}) }
这样业务方只需调用:
await useDialog('确定删除?')
无需关心组件实现,方便维护和替换。
六、减少模板污染 & 提升可维护性
声明式组件:
函数式调用:
MyDialog({ title, message }).then(confirmFn)
模板更干净;
状态逻辑转为函数逻辑;
对复用组件库开发非常友好。
✅ 总结对比表
| 优势 | 说明 |
|---|---|
| 使用简单 | 像函数一样调用,无需 v-if/v-show |
| 全局可用 | 任意位置可调用(即使无 Vue 上下文) |
| 逻辑解耦 | UI 与业务逻辑分离 |
| 实例独立 | 每次调用独立创建,不干扰其他实例 |
| 模板干净 | 不占用模板结构,适合全局弹窗类组件 |
| ⚙️ Promise 接口自然 | 可以方便地使用异步/等待用户操作 |
✅ 一句话总结:
函数式调用组件最大的优势是——
不受模板限制、调用灵活、使用简单,非常适合「全局弹出类 UI」场景。

浙公网安备 33010602011771号