1. 事件驱动基础:事件循环、事件队列与事件分发器

大家好,欢迎来到《UI事件驱动架构与适配实战》的第一章。

说实话,很多做UI的同学写了几年代码,天天跟 click、touch、scroll 打交道,但你要问他「事件到底是怎么从手指传到回调函数的?」——他可能就卡住了。

我当年也是这样。记得刚入行那会儿,我接手一个老项目,里面全是 window.addEventListener 和 setTimeout 混着用,bug 修都修不完。后来我才明白,不是我代码写得不好,是我根本不懂事件驱动那套底层逻辑。

所以这一章,咱们先把地基打牢。我会用最直白的方式,把事件循环、事件队列、事件分发器这三个核心概念讲清楚。你想想看,搞懂了这些,后面那些适配、架构、优化,全都是水到渠成的事。

1.1 事件驱动模型:UI 世界的「交通规则」

事件驱动模型,说白了就是「等事情发生,然后处理它」。跟传统的轮询模型不一样,事件驱动不需要 CPU 一直问「好了没?好了没?」,而是让系统安静地等着,事件来了再干活。

举个例子:你点了一下按钮。

  • 传统轮询:CPU 每秒问一万次「按钮被点了吗?」——浪费资源。
  • 事件驱动:系统睡大觉,直到你点了按钮,它才醒过来执行回调——高效。

UI 框架几乎全是事件驱动的。浏览器、Flutter、Qt、SwiftUI……底层都是这套逻辑。我个人习惯把事件驱动拆成三个角色:

角色 职责 类比
事件生产者 产生事件(用户点击、网络响应、定时器到期) 按下门铃的人
事件通道 传递和暂存事件(队列、总线) 门铃的导线
事件消费者 处理事件(回调函数、监听器) 屋里开门的人

核心要点:事件驱动模型的核心是「解耦」。生产者和消费者不需要知道对方的存在,它们只跟事件通道打交道。这为后面的适配和扩展打下了基础。

1.2 事件循环机制:UI 线程的「永动机」

事件循环,英文叫 Event Loop。它是整个 UI 系统的心脏,一个永不停止的循环。

我建议你把它想象成一个邮递员:

  1. 邮递员(事件循环)从邮筒(事件队列)里取信(事件)。
  2. 如果取到了,就按地址送过去(调用回调函数)。
  3. 送完了,再回来看看有没有新信。
  4. 如果没信,就坐在邮筒旁边打盹(阻塞等待)。

在浏览器里,事件循环的伪代码大概是这样的:

while (true) {
  // 1. 从队列里取一个事件
  event = eventQueue.dequeue();
  
  // 2. 如果没事件,就等着
  if (event == null) {
    waitForNewEvent();
    continue;
  }
  
  // 3. 处理事件
  dispatchEvent(event);
  
  // 4. 处理微任务(Promise、MutationObserver 等)
  processMicrotasks();
  
  // 5. 必要时更新渲染
  if (needsRender()) {
    renderFrame();
  }
}

嗯,这里要注意:事件循环不是 JavaScript 特有的。Flutter 的 PlatformDispatcher、Qt 的 QEventLoop、Android 的 Looper,底层都是这个模式。只是不同平台对「等待」和「优先级」的处理略有不同。

我的经验:我曾经在做一个跨平台 UI 库时,发现 iOS 和 Android 的事件循环在「空闲时」行为不一样。iOS 的 RunLoop 在空闲时会休眠,而 Android 的 Looper 会一直轮询。这个差异导致我们的动画在 Android 上多耗了 15% 的 CPU。后来我们在 Android 端加了一个空闲检测,才把功耗降下来。

1.3 事件队列:事件的「缓冲区」

事件队列,说白了就是一个先进先出的列表。所有事件都排着队,等着被事件循环处理。

为什么需要队列?直接调用回调不行吗?

你想想看,如果用户一秒内点了十下按钮,系统不可能同时处理十个回调。队列的作用就是「削峰填谷」——把突发的密集事件排成队,一个一个慢慢处理。

事件队列通常有两种:

类型 特点 典型场景
宏任务队列 普通事件、setTimeout、setInterval、I/O 用户点击、网络请求
微任务队列 高优先级,在宏任务之间执行 Promise.then、MutationObserver

我习惯把宏任务队列比作「普通通道」,微任务队列比作「VIP 通道」。每次事件循环处理完一个宏任务,会先把微任务队列清空,再去拿下一个宏任务。这就是为什么 Promise.then 总是比 setTimeout 先执行。

避坑指南:我曾经在一个项目中,用 setTimeout 递归调用实现了一个轮询功能。结果因为微任务队列里不断有 Promise 插入,导致 setTimeout 的回调被无限推迟。最后页面卡死了。记住:不要在微任务里无限生成新的微任务,否则宏任务永远没机会执行。

1.4 事件分发器:找到「对的人」

事件分发器(Event Dispatcher)的职责很简单:事件来了,找到该处理它的对象,然后调用对应的回调。

但「找到」这个过程,其实挺复杂的。以浏览器为例,一个点击事件要经过:

  1. 捕获阶段:从 window 一路向下,找到目标元素。
  2. 目标阶段:触发目标元素上的监听器。
  3. 冒泡阶段:从目标元素一路向上,回到 window。

这个过程叫「事件传播」。我建议你记住这张图:

事件传播三阶段 捕获阶段 冒泡阶段 window document body div button(目标) 事件先从上到下捕获,到达目标后,再从下到上冒泡

事件分发器的核心逻辑,其实就是遍历这个树形结构,找到所有绑定了对应事件的节点,然后依次调用它们的回调。

我建议你记住:分发器不负责「怎么处理事件」,它只负责「把事件送到该去的地方」。至于到了地方之后怎么处理,那是回调函数的事。

我的习惯:在写自定义 UI 组件时,我通常会自己实现一个轻量的事件分发器。核心就三个方法:on(注册)、off(注销)、emit(触发)。这样组件之间的通信就完全解耦了,后面想加适配层也方便。

1.5 三者如何协作?

事件循环、事件队列、事件分发器,这三者不是孤立的。它们配合起来,才构成了完整的事件驱动体系。

我画了一张协作流程图,你一看就明白:

事件驱动核心协作流程 事件生产者 用户点击 / 网络 / 定时器 产生事件 事件队列 FIFO 缓冲区 取出事件 事件循环 永动机 调用 事件分发器 找到目标 → 调用回调 触发 回调函数 处理事件 生产者 → 队列 → 循环 → 分发器 → 回调:一条完整的事件处理链

整个流程其实就五步:

  1. 生产者产生一个事件(比如用户点击)。
  2. 事件被放入事件队列的尾部。
  3. 事件循环从队列头部取出事件。
  4. 事件循环把事件交给事件分发器。
  5. 事件分发器找到对应的回调函数并执行。

就这么简单。但就是这套简单的机制,支撑起了整个 UI 世界的交互逻辑。

总结一下:事件循环是「发动机」,事件队列是「传送带」,事件分发器是「分拣员」。三者缺一不可。搞懂了它们,你就掌握了 UI 事件驱动的底层密码。

1.6 本章小结

这一章我们聊了事件驱动的三个核心概念:

  • 事件循环:永不停止的循环,负责从队列里取事件并处理。
  • 事件队列:先进先出的缓冲区,负责削峰填谷。
  • 事件分发器:负责找到目标对象并调用回调。

说实话,这些概念看起来简单,但真正用好它们,需要大量的实践。我在后面的章节里,会带着你一步步把这些概念落地到实际的 UI 适配场景中。

嗯,今天就到这儿。记住这张协作图,后面我们会反复用到它。