详细介绍:vue2和vue3函数式调用组件学习记录

对比说明

功能Vue 2 版本Vue 3 改写
动态创建Vue.extend + $mount()createVNode + render()
控制显示data.showref(show) + v-model:show
销毁组件$destroy()render(null, container)
暴露方法直接访问实例defineExpose()
语法风格Options API<script setup> + Composition API

一、vue2

使用 Vue.extend 将定义对象转为一个可实例化的构造函数,然后挂载到一个独立 DOM 节点。

我们以一个假期弹窗作为示例


<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


<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-ifv-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」场景。

posted @ 2025-11-19 15:27  gccbuaa  阅读(3)  评论(0)    收藏  举报