Skip to content

Dart EventLoop、Microtask Queue 与 Event Queue 机制 ​

回到总览:并发与运行时底座
相关模块:Dart Future、async/await 拆解与单线程并发 · Dart Isolate、compute() 与无共享多核并发

一句话定义 ​

Dart 语言采用了单线程事件循环 (Event Loop) 模型;每个 Dart Isolate 维护着一个 Event Loop 以及两个优先级不同的队列:Microtask Queue (微任务队列 - 优先级最高) 与 Event Queue (事件队列 - 优先级较低),驱动着 Flutter UI 渲染与所有异步 Task 的有序执行。

代码索引 ​

对应 Lab:dart-event-loop-isolate

为什么需要 ​

  • 为什么在 Dart 中 scheduleMicrotask() 里的任务永远比 Future() 先执行?
    • 一句话答:因为 Dart Event Loop 在处理 Event Queue 中的下一个事件之前,必须先清空 Microtask Queue 中的所有微任务;只有当 Microtask 队列完全为空时,才会去出队执行 Event 队列中的 Task。
  • 如果在 scheduleMicrotask() 中做死循环递归调用会导致什么严重后果?
    • 一句话答:由于 Microtask 队列拥有绝对优先权且会被连续清空,Microtask 死循环会导致 Event Queue 中的渲染与触摸事件永远无法被出队执行,导致 Flutter 界面瞬间冻结卡死(UI Starvation)。

底层机制 ​

1. Dart Event Loop 调度优先级模型 ​

调度规则:Microtask 队列(scheduleMicrotask() / Future.microtask())拥有绝对最高优先级,必须全部清空后才处理 Event 队列(I/O 回调、Timer、UI 渲染事件);Microtask 死循环会导致 Event 永远无法执行(UI 冻结)。

双队列属性对比表 ​

队列类型优先级常见触发 / 放入方式更适合承载什么放错后的典型后果
Microtask Queue绝对最高scheduleMicrotask()、Future.microtask()、已完成 Future 的 then() / await 后半段恢复轻量状态收尾、补一次异步边界、确保当前同步栈退出后立即续跑如果塞入耗时计算、递归补任务或批量重活,会饿死 Event Queue,导致 Flutter 掉帧、触摸延迟、界面卡死
Event Queue较低Future()、Future.delayed()、Timer.run()、Timer(...)、I/O 回调、平台消息、手势/点击、帧调度普通异步任务、延迟任务、I/O 完成通知、UI 事件、给渲染与输入留出机会如果把本应立即收尾的轻量逻辑全塞到 Event,顺序会被后续事件打断,容易出现状态更新晚一拍、日志顺序难读、时序判断失真

常见 API 应该记成什么 ​

  • scheduleMicrotask(() {}):最直接的微任务 API,明确表示“当前同步代码结束后立刻执行”。
  • Future.microtask(() {}):也是微任务,只是顺手返回一个 Future 便于链式处理。
  • Future(() {}):把任务放进 Event Queue,优先级低于全部 Microtask。
  • Future.delayed(...) / Timer.run(...) / Timer(...):本质都属于 Event 来源。
  • then():不是单独的第三种队列,而是“给 Future 注册完成回调”;它的特殊点在于:回调什么时候能执行,先取决于前面的 Future 何时完成;一旦 Future 完成,回调会以异步 continuation 的形式被调度,常见观察上会先于后续普通 Event。

then() 为什么容易让人误判 ​

then() 容易让人误以为“它总是微任务”或“它和 Future() 一样是 Event”,这两种说法都不准确。更准确的说法是:

  • then() 本身只负责注册回调,不单独创造线程,也不是单独的队列类型。
  • 如果前面的 Future 已经完成,例如 Future.value(1),那么 then() 的回调通常会在当前同步代码结束后很快执行,观察上更接近 Microtask。
  • 如果前面的 Future 本身来自 Future(() {})、Future.delayed(...)、I/O 或 Timer,那么必须先等那个 Event 发生、Future 完成,then() 才有机会继续。
  • await 本质上就是把“后半段代码”注册成 then() 风格的 continuation,所以“await 后为什么是异步恢复”与 “then() 为什么不是同步插队” 是同一套机制。

最短对比例子:

dart
import 'dart:async';

void main() {
  print('1. start');

  Future(() => print('2. event body'));
  Future.value('ready').then((_) => print('3. then after Future.value'));
  scheduleMicrotask(() => print('4. direct microtask'));

  print('5. end');
}

// 一种常见输出:
// 1. start
// 5. end
// 3. then after Future.value
// 4. direct microtask
// 2. event body

观察重点:

  • Future(() {}) 的函数体是普通 Event。
  • Future.value(...).then(...) 的回调不需要等待新的 Timer / I/O 事件,通常会在普通 Event 之前继续。
  • then() 的执行时机取决于“前面的 Future 什么时候完成”,而不是只看有没有写 then。

哪些任务适合放 Microtask,哪些适合放 Event ​

先记一个总原则:

  • Microtask 适合“当前同步代码刚结束就要立刻继续”的轻量收尾。
  • Event 适合“正常排队处理”的普通异步任务和外部事件。

更适合放到 Microtask 的任务:

  • 当前同步逻辑结束后,立刻补一次轻量状态收尾。
  • 保证回调异步化,但又不想晚到下一轮普通 Event。
  • await / then 恢复后的短逻辑,例如发一次轻量通知、拼一次最终状态。

更适合放到 Event Queue 的任务:

  • 普通异步任务、延迟任务、定时任务。
  • 用户点击、手势、文本输入、平台消息。
  • I/O 完成回调、网络返回、文件读取完成。
  • 帧调度、下一帧 build / layout / paint。
  • 可能稍重、希望给 UI 渲染和输入响应让路的逻辑。

经验判断:

  • Microtask 要“立即但很轻”。
  • Event 要“允许排队,别抢占 UI 的下一次响应机会”。
  • 一旦你怀疑“这段逻辑可能耗时、可能递归补任务、可能批量执行很多次”,就不要放 Microtask。

放错后的典型后果:

  • 把重活放到 Microtask:会饿死 Event Queue,导致 Flutter 掉帧、点击延迟、界面卡住。
  • 把本应立即收尾的轻逻辑全放到 Event:顺序容易被后续事件打断,出现“状态更新晚一拍”的时序问题。

Flutter / Dart 的统一事件处理心智模型 ​

可以把 Flutter 主 Isolate 想成一个“前台接待员”:

  • 主 Isolate:唯一真正执行 Dart UI 代码的前台。
  • Event Loop:前台的叫号系统。
  • Microtask Queue:前台手边优先级最高的小纸条。
  • Event Queue:正常排队的顾客、闹钟、电话、外部通知。

它的工作方式可以粗略理解成:

  1. 跑完当前同步代码。
  2. 清空 Microtask Queue。
  3. 取出一个 Event 执行。
  4. 再检查 Microtask。
  5. 没有任务时等待新的外部事件到来。

所以要先记住一句话:

  • 同一个 Isolate 在同一时刻只能执行一段 Dart 同步代码。

这意味着:

  • 用户点击回调里的同步方法
  • then() / await 恢复后的同步后处理
  • build() 里的同步计算
  • 大 JSON 解析、图片字节处理、排序聚合

本质上都会直接占住主 Isolate;只要它们没结束,新的点击、滚动、平台消息、下一帧绘制都只能排队等待。

为什么网络慢几秒,通常却不影响 UI 点击 ​

因为“等待网络返回”通常不是 Dart 主 Isolate 在原地同步傻等。更接近真实情况的理解是:

  • Dart 代码先发起异步请求。
  • 底层 Dart runtime + Flutter engine + 宿主系统 I/O 机制 去等待结果。
  • 等网络、文件或 socket 真正完成后,再把“完成事件”投回 Dart 主 Isolate 的 Event Loop。
  • Dart 再继续执行对应的 then() / await 后半段。

所以:

  • 等待网络 通常不占主 Isolate。
  • 处理网络结果 则可能占主 Isolate。

这也是为什么下面两种情况体验完全不同:

  • await http.get(...):大多只是等待,不一定卡 UI。
  • await 返回后立刻做大 JSON jsonDecode / 大量数据转换:很容易卡 UI。

同步方法是不是也算走 Event Loop ​

算,但要注意“走”的含义。

例如按钮点击:

dart
onPressed: () {
  doHeavyWork();
}

更准确的执行过程是:

  1. 点击先作为一个 Event 进入 Event Loop。
  2. Event Loop 取出这个点击事件,开始执行 onPressed。
  3. onPressed 里的同步方法 doHeavyWork() 在当前调用栈里直接跑完。
  4. 只有它返回之后,Event Loop 才能继续处理下一个事件。

所以如果 doHeavyWork() 持续 1 秒,那么这 1 秒里:

  • 新的点击只能排队。
  • 滚动响应只能排队。
  • 下一帧绘制也只能排队。

这就是 Flutter 卡顿最常见的根因:不是 Event Loop 本身卡了,而是主 Isolate 被同步 CPU 工作长时间占住了。

Flutter 中常见场景可以如何归类 ​

场景更适合怎么理解为什么
scheduleMicrotask / Future.microtaskMicrotask当前同步代码结束后立刻执行
Future() / Future.delayed() / TimerEvent普通异步任务与定时任务
点击、拖拽、滚动、文本输入Event来自引擎 / 平台的外部输入事件
MethodChannel / 生命周期通知Event宿主平台把消息投回 Dart isolate
网络 / 文件 / socket 完成通知Event等底层 I/O 完成后再入队
await / then 恢复后的后半段常见观察上更接近 continuationFuture 完成后续跑,不等同于新外部事件
setState() 后的重建、VSync 驱动的 frameEvent属于帧调度链路
addPostFrameCallbackEvent(帧尾回调)依附于 frame pipeline,不是 Microtask

一句话判断是否会卡 UI ​

可以先问一句:

  • 这段时间里,主 Isolate 是在“等待外部结果”,还是在“自己同步算东西”?

如果是:

  • 等待外部结果:通常不太会直接卡 UI,例如网络等待、文件读取等待、Timer 等待。
  • 自己同步算东西:就很容易卡 UI,例如大 JSON 解析、图片处理、复杂排序聚合、重 build、微任务递归。

代码示例与执行顺序解析 ​

dart
import 'dart:async';

void main() {
  print('1. Main start');

  Future(() => print('2. Future 1 (Event)'));

  Future.microtask(() => print('3. Microtask 1'));

  scheduleMicrotask(() => print('4. Microtask 2'));

  Future(() => print('5. Future 2 (Event)'));

  print('6. Main end');
}

// 打印结果:
// 1. Main start
// 6. Main end
// 3. Microtask 1
// 4. Microtask 2
// 2. Future 1 (Event)
// 5. Future 2 (Event)

Android / Flutter / Web / Backend 对照 ​

维度Dart (Flutter)JavaScript (Node/Web)Android Native
单线程事件循环Isolate 内 EventLoopV8 Event LoopHandler / MessageQueue
微任务概念Microtask QueuePromise.then / process.nextTick无直接微任务 (由 Message 优先级代替)
主队列概念Event QueueMacrotask Queue (setTimeout/IO)MessageQueue (Looper 出队)

如果你已经理解了主 isolate 上的 event loop / microtask / event queue,但还在判断“这段重活是不是该搬到后台 isolate”,建议继续看 Dart 并发总览:Future / event loop / isolate 的选型总览,以及 Dart Isolate、compute() 与无共享多核并发 的 Flutter 场景说明。

对应实验 ​

各章节内已嵌入跳转链接;此处汇总全部 Lab:

Lab说明
dart-event-loop-isolateEventLoop / Microtask Queue / Event Queue / Isolate 调度顺序验证

复习检查题 ​

  1. 在 Dart 中,微任务队列 (Microtask Queue) 与事件队列 (Event Queue) 的执行顺序是怎样的?

    答:微任务队列拥有绝对优先执行权。在事件循环的每一次轮询中,Dart 必须先将微任务队列中的所有任务全部执行并清空,然后才会去事件队列中取出 1 个事件执行;每执行完 1 个事件,系统又会立即检查并清空微任务队列。

  2. 为什么在 Flutter 中不推荐在 scheduleMicrotask() 中放置耗时或递归任务?

    答:因为 Event Loop 在清空 Microtask 队列之前不会处理任何 Event Queue 中的任务。如果 Microtask 队列中有耗时计算或递归任务,会导致 Event Queue 中的 UI 绘制(VSync)和用户触摸点击事件无法被及时处理,从而引发严重的 Flutter 界面冻结和丢帧。

速记 ​

  • 优先原则:Microtask 拥有绝对优先权,必须完全清空才轮到 Event。
  • 两队列分工:Microtask 放轻量状态同步,Event 放 Future/Timer/UI 事件。
  • 卡死避坑:严禁 Microtask 死循环,耗时重载移步 Isolate。

站点构建时间:2026/8/24 23:43:17