Message消息机制的实现原理与前端应用实践
项目初期的技术选型
上个月接了个后台管理系统重构的活,UI 组件库用的是 Ant Design,但客户提了个需求:全局消息提示(Message)要能自定义样式,还要支持“批量操作成功后合并提示”——比如“3 条数据删除成功”,而不是连续弹 3 个“删除成功”。一开始我直接用了 AntD 的 message.success(),结果发现它不支持动态合并,每次调用都新建一个实例,堆在页面上密密麻麻,用户反馈体验很差。
纠结了一下,是魔改 AntD 还是自己写?考虑到后续可能还要加动画、位置控制、点击关闭等功能,干脆自己撸一个。反正 Message 逻辑不复杂:创建 DOM、显示、定时销毁。但真动手才发现,坑比想象中多。
核心代码就这几行(理想很丰满)
最开始的实现特别简单:搞个全局容器,每次调用 showMessage 就往里面塞个 div,加个 CSS 动画,3 秒后自动删掉。代码大概长这样:
// 简化版,实际项目有更多配置
let container = null;
function initContainer() {
if (!container) {
container = document.createElement('div');
container.className = 'message-container';
document.body.appendChild(container);
}
}
function showMessage(text, type = 'info') {
initContainer();
const msg = document.createElement('div');
msg.className = message message-${type};
msg.textContent = text;
container.appendChild(msg);
// 淡入动画
setTimeout(() => msg.classList.add('show'), 10);
// 3秒后移除
setTimeout(() => {
msg.classList.remove('show');
setTimeout(() => msg.remove(), 300); // 等动画结束再删
}, 3000);
}
.message-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
}
.message {
padding: 8px 16px;
margin-bottom: 8px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
opacity: 0;
transform: translateY(-10px);
transition: all 0.3s;
}
.message.show {
opacity: 1;
transform: translateY(0);
}
跑起来没问题,但一压测就露馅了——快速点十几次按钮,DOM 节点疯狂堆积,内存蹭蹭涨。更糟的是,如果用户在消息消失前关掉页面,那些 setTimeout 还在后台跑,虽然不影响功能,但看着控制台一堆报错(试图操作已删除的 DOM),心里发毛。
最大的坑:性能问题和重复提示
第一个问题是性能。我原以为 Message 生命周期短,GC 会自动回收,但实测发现频繁创建/销毁 DOM 开销不小,尤其在低端安卓机上卡顿明显。后来改成**对象池**:预创建 5 个消息节点,用完放回池里复用,不用每次都 new DOM。关键代码:
const pool = [];
const MAX_POOL_SIZE = 5;
function getMessageFromPool() {
return pool.length > 0 ? pool.pop() : document.createElement('div');
}
function returnMessageToPool(msg) {
if (pool.length < MAX_POOL_SIZE) {
msg.className = '';
msg.textContent = '';
pool.push(msg);
} else {
msg.remove(); // 池满了就真删
}
}
// 在 showMessage 里用 getMessageFromPool() 代替 createElement
// 在销毁时用 returnMessageToPool(msg) 代替 msg.remove()
这招效果立竿见影,内存占用降了 60%,低端机也不卡了。
第二个坑是重复提示。比如用户连点“保存”,弹出 5 个“保存成功”。产品要求合并成“保存成功 x5”。我折腾了半天,最后用**防抖+计数器**搞定:相同内容的消息在 1 秒内只显示一个,后面来的只更新计数。难点在于怎么判断“相同内容”——不能只比字符串,因为“删除成功”和“删除成功”是一样的,但“删除 id=1 成功”和“删除 id=2 成功”就不一样。最后妥协:只对固定文案(如“操作成功”)做合并,带变量的不管。实现时用个 Map 缓存最近的消息:
const recentMessages = new Map(); // key: text, value: { count, timer }
function showMessage(text, type = 'info') {
// 如果是可合并的文案(比如以"成功"结尾)
if (isMergeable(text)) {
if (recentMessages.has(text)) {
const record = recentMessages.get(text);
record.count++;
// 更新显示文本
record.element.textContent = ${text} x${record.count};
// 重置倒计时
clearTimeout(record.timer);
record.timer = setTimeout(() => {
destroyMessage(record.element, text);
}, 3000);
return;
}
}
// ...创建新消息逻辑
const element = createMessageElement(text, type);
container.appendChild(element);
let timer = setTimeout(() => {
destroyMessage(element, text);
}, 3000);
if (isMergeable(text)) {
recentMessages.set(text, { count: 1, element, timer });
}
}
function destroyMessage(element, text) {
element.classList.remove('show');
setTimeout(() => {
returnMessageToPool(element);
if (isMergeable(text)) {
recentMessages.delete(text);
}
}, 300);
}
这里注意我踩过好几次坑:忘记在销毁时清理 recentMessages,导致内存泄漏;还有合并时没考虑不同类型(比如 success 和 error 不该合并),后来加了 type 作为 key 的一部分才解决。
最终的解决方案
综合下来,现在的方案是:
- 用对象池管理 DOM 节点,避免频繁创建
- 对固定文案做 1 秒内的合并提示
- 所有定时器在组件销毁时统一清理(虽然 Message 是全局的,但我在 SPA 路由切换时会手动清空容器)
- 加了点击关闭功能:用户点消息就立刻消失,不用等 3 秒
点击关闭很简单,给每个消息加个 click 事件:
msg.addEventListener('click', () => {
clearTimeout(timer); // 清掉倒计时
destroyMessage(msg, text);
});
另外,为了适配不同场景,还加了配置项:持续时间、位置(top/right/bottom)、是否可关闭。调用时可以这么写:
showMessage('数据保存成功', {
type: 'success',
duration: 5000, // 5秒
closable: true,
position: 'top-center'
});
回顾与反思
整体效果还不错,用户没再抱怨消息刷屏,性能也达标。但有几个小问题没完全解决:
- 合并提示的规则太死板,只能处理固定文案。如果产品哪天要合并“删除用户[张三]成功”这种,还得改逻辑。不过目前需求没到这一步,先放着。
- 对象池大小写死为 5,极端情况(比如同时弹 10 个消息)还是会创建新节点。但实测 99% 场景够用,懒得动态扩容了。
- 没做 SSR 兼容(因为是后台系统,全是 CSR),如果用在 Next.js 项目里得加
typeof window !== 'undefined'判断。
其实 AntD 的 Message 也能通过 message.config 改位置、时长,但合并提示和对象池这种深度定制,还是自己写更灵活。这次折腾最大的收获是:**别小看简单组件,高频使用下性能细节决定体验**。以前总觉得“弹个消息而已,能有多大事”,现在看到 createElement 就手抖。
以上是我踩坑后的总结,希望对你有帮助。如果你们有更好的合并提示方案,或者遇到过类似性能问题,欢迎评论区交流!

暂无评论