原生 DOM 实现 toast功能和vue3组件方式实现toast功能对比

2 阅读3分钟

纯原生 DOM 版本 toast.js(不需要 MyToast.vue 组件)

/**
 * toast 消息提示(纯DOM版本,不依赖SFC组件)
 * @param {string} msg 提示文字
 * @param {'success'|'error'|'warning'|'info'} [type='info']
 * @param {number} [duration=2500]
 */
export function toast(msg, type = 'info', duration = 2500) {
  // 1. 创建dom元素
  const wrap = document.createElement('div')
  wrap.className = 'toast-wrap'

  const box = document.createElement('div')
  box.className = `toast-box ${type}`
  box.innerText = msg

  wrap.appendChild(box)
  document.body.appendChild(wrap)

  // 2. 定时移除dom
  const timer = setTimeout(() => {
    wrap.remove()
    clearTimeout(timer)
  }, duration)
}

// 快捷别名
toast.success = (msg, d) => toast(msg, 'success', d)
toast.error = (msg, d) => toast(msg, 'error', d)
toast.warning = (msg, d) => toast(msg, 'warning', d)
toast.info = (msg, d) => toast(msg, 'info', d)

样式,放到全局 css(main.css)

.toast-wrap {
  position: fixed;
  top: 40px;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  z-index: 9999;
}
.toast-box {
  padding: 10px 20px;
  border-radius: 6px;
  color: #fff;
  font-size: 14px;
  box-shadow: 0 2px 12px rgba(0,0,0,0.15);
}
.toast-box.success { background-color: #67c23a; }
.toast-box.error { background-color: #f56c6c; }
.toast-box.warning { background-color: #e6a23c; }
.toast-box.info { background-color: #909399; }

页面直接调用:

import { toast } from '@/utils/toast'
toast.success('保存成功')

✅ 效果和之前组件版一模一样,DOM 直接 append 到 body,没有 Vue 组件参与。


两种方案对比:原生 DOM vs Vue 组件 (h+render+Teleport)

方案 1:直接操作 body DOM(上面这个)

✅优点:

  1. 最简单,没有 Vue 组件开销,不需要.vue文件,不需要h/render/Teleport
  2. 理解门槛低,就是原生 JS DOM API。

❌缺点:

  1. 没有响应式:如果 toast 内容要动态变化、倒计时、状态切换,要手动操作innerTextclassName
  2. 样式不能用scoped,只能全局 css。
  3. 没有 Vue 生命周期:关闭就是wrap.remove(),没有onUnmounted,定时器要自己手动清理,很容易内存泄漏。
  4. 复杂能力很难写:过渡动画、关闭按钮、队列多条消息、图标,全部要手写 DOM 操作,代码会越来越乱。
  5. 和 Vue 生态割裂:不能用v‑ifv‑bind等模板语法。

适合:简单 toast,只展示一段文字,不需要复杂交互。

方案 2:MyToast.vue + h + render + Teleport(上一套方案)

✅优点:

  1. 完整使用 Vue 能力:模板、scoped 样式、watch、props 校验、生命周期。
  2. 声明式开发,不用手动操作 DOM。
  3. 想加动画、图标、复杂布局,直接改 vue 组件即可。

❌缺点:

  1. 概念多:VNode、h、render、Teleport,学习成本高。
  2. 有 Vue 组件实例的微小开销。

适合:企业级组件,后续会迭代功能,Element‑Plus ElMessage 就是这套思路。


关键问题:直接操作 body 有什么坑?

  1. 内存泄漏风险
const timer = setTimeout(()=>wrap.remove(),2500)
// 如果页面卸载的时候toast还没消失,定时器还在跑,dom已经移除,定时器依旧存在。

原生 DOM 版本,没有组件生命周期,页面销毁时你需要自己清除定时器;而 Vue 组件版本,组件卸载会自动清理 watch 里面的 timer。

  1. DOM 堆积 每次调用toast()都会document.body.appendChild(wrap),每调用一次新增一个 DOM 节点;简单版本多条 toast 会堆叠,这个本身没问题,但要记得remove()
  2. 样式污染 不能用scoped,class 名字要起独特,避免和页面其他 class 冲突。

什么时候选哪个?

  1. 快速写个简单提示、小 demo:直接操作 body DOM 完全够用
  2. 项目长期维护、后续要加动画、图标、复杂逻辑:用 SFC 组件 + h + render + Teleport

补充:Teleport 本质底层也是操作body,只不过DOM 移动操作由 Vue 帮你完成,你不用手写 appendChild

通俗总结

  • 直接操作 body:你手写document.createElement + body.appendChild,自己管理 DOM。
  • Teleport:Vue 内部帮你做 append/remove,你只写模板。
  • h+render:帮你动态实例化 Vue 组件。