在前端面试中,Toast 组件实现是高频考点 —— 它看似简单,却能考察候选人对「跨组件通信」「内存管理」「时序控制」的理解深度。之前的文字版偏重于代码,这次补充「流程图 + 逻辑示意图」,帮你直观掌握核心逻辑,面试时能快速梳理思路。
一、基础:为什么选 mitt 做事件总线?
要实现 Toast 的「全局调用 + 局部显示」,首先需要一个轻量的事件中间件。mitt 是最优解之一,先看它的核心优势:
| 特性 | 说明(面试必提) | 对比其他方案(如 Vue Event Bus) |
|---|---|---|
| 体积 | 仅~200B,无依赖 | Vue2 Event Bus 需依赖 Vue 实例,体积更大 |
| 兼容性 | 支持 IE9+,适配老项目 | 部分方案(如 React Context)依赖框架版本 |
| API 设计 | 仅on/off/emit,无学习成本 | 框架内置方案可能需理解生命周期绑定 |
| 灵活性 | 可创建多实例,避免事件名冲突 | Vue2 Event Bus 全局单实例,易冲突 |
mitt 核心原理:发布 - 订阅模式(流程图)
Toast 的通信本质是「发布者触发事件,订阅者(Toast 组件)响应」
flowchart TD
Start([开始]) --> P1[调用 toast.show message, duration]
subgraph Publisher[发布者]
P1 --> P2[触发事件]
end
P2 -->|emit| EC[事件中心<br/>EventEmitter]
subgraph Subscriber[订阅者]
S1[Toast 组件] -->|监听 show 事件| S2[接收通知]
S2 --> S3[setState visible: true]
end
EC -->|notify| S1
S3 --> Render[渲染组件]
Render --> Show[显示 Toast]
Show --> Timer[setTimeout]
Timer --> Hide[隐藏 Toast]
Hide --> End([结束])
style Publisher fill:#e3f2fd
style Subscriber fill:#f3e5f5
style EC fill:#fff9c4
面试考点:为什么不直接用全局变量控制 Toast 显示?
→ 全局变量会导致「状态污染」,且无法灵活处理多组件同时调用(比如两个按钮同时触发 Toast);而发布 - 订阅模式能解耦发布者和订阅者,每个订阅者独立响应,且支持多发布者。
二、核心:Toast 事件通信与内存管理(图解 + 代码)
这部分是面试重点 ——如何避免事件监听导致的内存泄漏,必须结合「组件生命周期」讲清楚。
2.1 Vue3 版本:生命周期与事件绑定的时序图
流程图标注「监听何时加、何时清」:
生命周期与事件绑定的时序图
sequenceDiagram
autonumber
participant Mount as 组件挂载
participant Toast as Toast组件
participant EventBus as EventEmitter
participant API as toast.show()
participant State as React State
rect rgb(200, 230, 255)
Note right of Mount: useEffect 订阅阶段
Mount->>Toast: 首次渲染
Toast->>EventBus: eventBus.on('show', handler)
EventBus-->>Toast: 返回监听器引用
Toast->>Toast: 保存 cleanup 函数
end
rect rgb(255, 240, 200)
Note right of API: 业务调用阶段
API->>EventBus: eventBus.emit('show', config)
EventBus->>Toast: 执行 handler(config)
Toast->>State: setState({visible, message})
State->>Toast: 触发重新渲染
Toast-->>API: Toast 显示成功
end
rect rgb(230, 255, 230)
Note right of Toast: 定时隐藏阶段
Toast->>Toast: setTimeout(hide, 3000)
Note over Toast: 3秒后
Toast->>State: setState({visible: false})
State->>Toast: 重新渲染(隐藏)
end
rect rgb(255, 230, 230)
Note right of Mount: useEffect cleanup 阶段
Mount->>Toast: 组件卸载
Toast->>Toast: 执行 cleanup 函数
Toast->>EventBus: eventBus.off('show', handler)
EventBus-->>Toast: 移除监听成功
Toast->>Toast: clearTimeout(timer)
Note over Toast,EventBus: 清理完成,无内存泄漏
end
2.2 React 版本:useEffect 与事件清理(逻辑示意图)
React 中用useEffect的「清理函数」替代生命周期钩子,核心逻辑和 Vue 一致,但需注意依赖项为空数组(确保只订阅一次):
// Toast.jsx
import { useState, useEffect } from 'react';
import emitter from '@/utils/eventBus';
import './Toast.css';
const Toast = () => {
const [state, setState] = useState({ isVisible: false, msg: '', type: 'info' });
let timer = null;
const handleShow = (options) => {
setState({ ...options, isVisible: true });
if (timer) clearTimeout(timer);
timer = setTimeout(() => setState(prev => ({ ...prev, isVisible: false })), 2000);
};
useEffect(() => {
emitter.on('toast:show', handleShow);
return () => {
emitter.off('toast:show', handleShow);
clearTimeout(timer);
};
}, []);
return (
<div className={`toast toast--${state.type} ${state.isVisible ? 'toast--show' : ''}`}>
{state.msg}
</div>
);
};
export default Toast;
React 事件清理逻辑示意图
plaintext
┌─────────────────────────────────────────┐
│ useEffect(() => { │
│ // 1. 组件挂载时:订阅事件 │
│ emitter.on('toast:show', handleShow); │
│ │
│ // 2. 清理函数:组件卸载时执行 │
│ return () => { │
│ emitter.off('toast:show', handleShow);│ ← 必须!防止内存泄漏
│ clearTimeout(timer); │ ← 防止定时器回调异常
│ }; │
│ }, []); │
└─────────────────────────────────────────┘
三、高频坑点:优化方案可视化
面试中常问「Toast 有哪些常见问题?如何解决?」,用「问题 + 图解 + 方案」的结构,记忆更深刻。
坑点 1:多次触发导致的「定时器叠加」
问题:短时间内连续调用emitter.emit('toast:show'),会创建多个定时器,导致 Toast 提前隐藏或闪烁。
错误时序(未清理定时器):
0s第一次emit →定时器1(2s后隐藏)0.5s第二次emit →定时器2(2s后隐藏)2s定时器1触发 →Toast隐藏(但定时器2还在)2.5s定时器2触发 →Toast再次隐藏(异常闪烁)未清理定时器的问题
解决方案:每次触发handleShow时,先清理旧定时器:
const handleShow = (options) => {
setState({ ...options, isVisible: true });
if (timer) clearTimeout(timer); // 关键:清除旧定时器
timer = setTimeout(() => setState(prev => ({ ...prev, isVisible: false })), 2000);
};
优化后时序:
0s第一次emit →定时器1(2s后隐藏)0.5s第二次emit → 清除定时器1→新建定时器2(2s后隐藏)2.5s定时器2触发 →Toast隐藏(正常)清理定时器后的正常时序
坑点 2:事件监听导致的「内存泄漏」
问题:如果组件卸载时未调用emitter.off,mitt 总线会一直持有组件的handleShow引用,导致组件实例无法被 GC(垃圾回收),内存越积越大。
内存泄漏示意图:
┌─────────────┐ 持有引用 ┌─────────────┐
│ mitt总线 │──────────────→ │ handleShow │
└─────────────┘ └─────────────┘
↓
┌─────────────┐
│ Toast组件实例│(无法被GC回收)
└─────────────┘
解决方案:组件卸载时必须取消订阅(Vue 的onUnmounted/React 的useEffect清理函数),示意图:
┌─────────────┐ 取消引用 ┌─────────────┐
│ mitt总线 │──────────────→ │ handleShow │
└─────────────┘ └─────────────┘
↓
┌─────────────┐
│ Toast组件实例│(可被GC正常回收)
└─────────────┘
四、高级扩展:Toast 队列显示(面试加分项)
如果面试问「如何处理多个 Toast 同时触发?」,可以讲「队列调度」方案,用流程图展示逻辑:
队列实现核心逻辑
// eventBus.js 扩展队列功能
import mitt from 'mitt';
const emitter = mitt();
// 队列管理
let toastQueue = []; // 存储待显示的Toast
let isShowing = false; // 标记当前是否有Toast在显示
// 处理队列的核心函数
const processQueue = () => {
if (isShowing || toastQueue.length === 0) return;
isShowing = true;
const currentToast = toastQueue.shift(); // 取出第一个Toast
emitter.emit('toast:show', currentToast); // 显示当前Toast
// 2秒后处理下一个
setTimeout(() => {
isShowing = false;
processQueue(); // 递归处理队列
}, 2000);
};
// 对外暴露的调用方法
export const showToast = (options) => {
toastQueue.push(options); // 加入队列
processQueue(); // 尝试处理队列
};
export default emitter;
面试亮点:队列方案避免了多个 Toast 重叠显示,提升用户体验,同时体现了「异步时序控制」的能力。
五、面试总结:核心考点图谱
最后用一张「考点图谱」帮你梳理所有重点,面试时可按这个逻辑回答:
通过「文字 + 流程图」的结合,既能直观理解核心逻辑,又能在面试中快速梳理思路。记住:面试官考察 Toast 组件,本质是看你对「解耦」「内存安全」「异步控制」的理解 —— 这些底层能力比代码本身更重要。