Solution:用户无感,数据排队处理
我们想象你的餐厅刚开门,后台 POS 系统还在启动,但门口已经有顾客进来点单了。服务员不能说"系统没好,这笔单不算"——他们先用手写单记录下来,等 POS 系统就绪后,再把手写单一笔一笔录进去。
那叠手写单就是代码里的"队列",就绪后的 POS 系统(实际的数据后端,比如 Datadog)就是"水槽(Sink)"。从代码上看,src/services/analytics/index.ts 是整个分析系统的入口,其他所有模块调用 logEvent() 来记录事件:
// src/services/analytics/index.ts
const eventQueue: QueuedEvent[] = [] // 手写单堆放处
let sink: AnalyticsSink | null = null // POS 系统,启动时为空
export function logEvent(eventName: string, metadata: LogEventMetadata): void {
if (sink === null) {
eventQueue.push({ eventName, metadata, async: false }) // POS 没好:先写手写单
return
}
sink.logEvent(eventName, metadata) // POS 好了:直接录入
}
POS 就绪后,相当于服务员收到通知"可以开始录手写单了"——但不是立刻放下正在服务的顾客,而是等这一桌结完账再去:
// src/services/analytics/index.ts
export function attachAnalyticsSink(newSink: AnalyticsSink): void {
if (sink !== null) return // 幂等:POS 已经开了,重复通知不处理
sink = newSink
if (eventQueue.length > 0) {
const queuedEvents = [...eventQueue]
eventQueue.length = 0
queueMicrotask(() => { // 异步冲刷:等这轮忙完再录,不耽误正在进行的服务
for (const event of queuedEvents) {
sink!.logEvent(event.eventName, event.metadata)
}
})
}
}
queueMicrotask 正是"等当前这轮任务做完,插到下一个空隙再处理"的机制。启动时每毫秒都很宝贵,不能因为冲刷历史事件而拖慢启动速度。