1、事件驱动模型:什么是事件驱动?事件循环(Event Loop)的工作原理

各位同学,咱们今天聊一个非常核心的概念——事件驱动模型。说实话,我刚开始做动画开发那会儿,对这东西也是一知半解。直到有一次,我写了一个复杂的交互动画,结果页面卡得跟幻灯片似的……嗯,从那以后,我才真正沉下心来把事件驱动搞明白。

说白了,事件驱动就是一种编程范式。它的核心思想很简单:系统不主动干活,而是等着“事情”发生,再去响应。这个“事情”就是事件——鼠标点击、键盘按下、定时器到期、网络请求返回……统统算事件。

1.1 事件驱动 vs 传统轮询

为了让你理解得更透彻,我拿一个生活中的例子来说。

假设你在等外卖:

  • 轮询模式:你每隔 5 分钟给外卖小哥打个电话:“到哪了?到哪了?”——CPU 空转,浪费资源。
  • 事件驱动模式:你该干嘛干嘛,等外卖小哥到了给你打电话(事件),你再下楼去拿(响应)。

你想想看,哪个更高效?显然是后者。事件驱动模型就是这种“被动响应”的哲学。

对比维度 轮询模式 事件驱动模式
资源占用 持续占用 CPU 空闲时释放 CPU
响应延迟 取决于轮询间隔 事件发生即响应
代码复杂度 简单但低效 需要回调/监听机制
适用场景 简单状态检查 高并发、UI 交互、I/O 操作

核心要点:事件驱动模型的核心三要素——事件源(谁产生事件)、事件监听器(谁关心事件)、事件处理器(事件来了怎么办)。

1.2 事件循环(Event Loop)的工作原理

事件驱动模型能跑起来,全靠一个幕后英雄——事件循环。我习惯把它比作一个“永不休息的调度员”。

它的工作流程其实很简单,就三步:

  1. 检查事件队列:看看有没有新事件排队等着处理。
  2. 取出事件:从队列头部拿一个事件出来。
  3. 执行回调:调用事件对应的处理器函数。

然后重复 1→2→3→1→2→3……直到程序退出。这就是事件循环。

我的经验:我曾经在做一个实时协作白板项目时,发现事件循环被一个耗时操作阻塞了,导致所有绘制事件都卡住。解决办法是把耗时操作拆成微任务(microtask)或者扔到 Web Worker 里。记住:事件循环最怕“长任务”。

这里有一个关键点:事件循环是单线程的。JavaScript 就是典型的例子——只有一个主线程在跑事件循环。你可能会问:“单线程怎么处理并发?”答案是:异步 + 非阻塞 I/O。

来看一段伪代码,模拟事件循环的核心逻辑:

// 事件循环的简化实现
while (true) {
  // 1. 从队列中取出一个事件
  const event = eventQueue.dequeue();
  
  if (event) {
    // 2. 执行事件对应的回调
    const handler = eventHandlers[event.type];
    if (handler) {
      handler(event.data);
    }
  }
  
  // 3. 处理微任务队列(Promise、MutationObserver 等)
  while (microtaskQueue.length > 0) {
    const microtask = microtaskQueue.dequeue();
    microtask();
  }
  
  // 4. 如果队列为空,进入休眠等待新事件
  if (eventQueue.isEmpty() && microtaskQueue.isEmpty()) {
    waitForNewEvent();
  }
}

注意:上面的代码是简化版。真实的事件循环(比如浏览器或 Node.js)还要处理宏任务(macrotask)和微任务(microtask)的优先级。微任务会在每次宏任务结束后立即执行,优先级高于下一个宏任务。

1.3 事件驱动在动画刷新中的应用

好了,理论讲完了,咱们回到动画这个主题上。动画刷新中的事件响应,说白了就是:每一帧的渲染都是由事件驱动的。

在浏览器里,requestAnimationFrame 就是一个典型的事件驱动 API。你注册一个回调函数,浏览器会在每次准备刷新屏幕时“通知”你——这就是一个事件。

// 一个简单的动画循环
function animate(timestamp) {
  // 更新动画状态
  updateAnimation(timestamp);
  
  // 请求下一帧
  requestAnimationFrame(animate);
}

// 启动动画
requestAnimationFrame(animate);

你想想看,如果没有事件驱动,你要怎么实现动画?只能用 setInterval 轮询,但那样你永远不知道浏览器什么时候真正刷新屏幕,很容易出现掉帧或者撕裂。

下面这张图展示了事件驱动模型在动画刷新中的完整流程:

事件驱动模型在动画刷新中的工作流程 事件源 事件类型 • 鼠标点击/移动 • 键盘按键 • 定时器到期 • requestAnimationFrame 事件队列 FIFO 先进先出 排队等待处理 优先级:微任务 > 宏任务 事件循环 1. 取出事件 2. 执行回调 3. 处理微任务 事件处理器(回调函数) 动画帧渲染 循环请求下一帧 💡 关键点 事件循环必须快速完成 否则会导致动画掉帧

1.4 事件驱动模型的优缺点

任何技术都有两面性,事件驱动也不例外。我在项目中踩过不少坑,这里给你总结一下:

优点 缺点
资源利用率高,空闲时不占 CPU 回调地狱(Callback Hell),代码可读性差
响应及时,事件发生即处理 调试困难,事件流难以追踪
天然适合 UI 交互场景 错误处理复杂,一个未捕获的异常可能搞崩整个循环
扩展性好,可以轻松添加新的事件监听 内存泄漏风险——忘记移除事件监听

我曾经踩过的坑:有一次我做了一个长列表的滚动动画,给每个列表项都绑定了 scroll 事件监听。结果列表项销毁时忘了移除监听,导致内存泄漏,页面越来越卡。后来我养成了一个习惯:所有事件监听都要成对出现——绑定和移除。

1.5 小结

事件驱动模型,说白了就是“等事做事”。事件循环是它的心脏,不停地从队列里取事件、执行回调。在动画刷新中,requestAnimationFrame 就是最典型的事件驱动 API——浏览器告诉你“该画下一帧了”,你再去更新画面。

理解了这个模型,后面咱们聊动画帧的调度、事件优先级、防抖节流……你都会觉得顺理成章。嗯,今天就先到这儿,下节课见。