Appearance
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:正常排队的顾客、闹钟、电话、外部通知。
它的工作方式可以粗略理解成:
- 跑完当前同步代码。
- 清空 Microtask Queue。
- 取出一个 Event 执行。
- 再检查 Microtask。
- 没有任务时等待新的外部事件到来。
所以要先记住一句话:
- 同一个 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返回后立刻做大 JSONjsonDecode/ 大量数据转换:很容易卡 UI。
同步方法是不是也算走 Event Loop
算,但要注意“走”的含义。
例如按钮点击:
dart
onPressed: () {
doHeavyWork();
}更准确的执行过程是:
- 点击先作为一个 Event 进入 Event Loop。
- Event Loop 取出这个点击事件,开始执行
onPressed。 onPressed里的同步方法doHeavyWork()在当前调用栈里直接跑完。- 只有它返回之后,Event Loop 才能继续处理下一个事件。
所以如果 doHeavyWork() 持续 1 秒,那么这 1 秒里:
- 新的点击只能排队。
- 滚动响应只能排队。
- 下一帧绘制也只能排队。
这就是 Flutter 卡顿最常见的根因:不是 Event Loop 本身卡了,而是主 Isolate 被同步 CPU 工作长时间占住了。
Flutter 中常见场景可以如何归类
| 场景 | 更适合怎么理解 | 为什么 |
|---|---|---|
scheduleMicrotask / Future.microtask | Microtask | 当前同步代码结束后立刻执行 |
Future() / Future.delayed() / Timer | Event | 普通异步任务与定时任务 |
| 点击、拖拽、滚动、文本输入 | Event | 来自引擎 / 平台的外部输入事件 |
MethodChannel / 生命周期通知 | Event | 宿主平台把消息投回 Dart isolate |
| 网络 / 文件 / socket 完成通知 | Event | 等底层 I/O 完成后再入队 |
await / then 恢复后的后半段 | 常见观察上更接近 continuation | Future 完成后续跑,不等同于新外部事件 |
setState() 后的重建、VSync 驱动的 frame | Event | 属于帧调度链路 |
addPostFrameCallback | Event(帧尾回调) | 依附于 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 内 EventLoop | V8 Event Loop | Handler / MessageQueue |
| 微任务概念 | Microtask Queue | Promise.then / process.nextTick | 无直接微任务 (由 Message 优先级代替) |
| 主队列概念 | Event Queue | Macrotask 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-isolate | EventLoop / Microtask Queue / Event Queue / Isolate 调度顺序验证 |
复习检查题
在 Dart 中,微任务队列 (Microtask Queue) 与事件队列 (Event Queue) 的执行顺序是怎样的?
答:微任务队列拥有绝对优先执行权。在事件循环的每一次轮询中,Dart 必须先将微任务队列中的所有任务全部执行并清空,然后才会去事件队列中取出 1 个事件执行;每执行完 1 个事件,系统又会立即检查并清空微任务队列。
为什么在 Flutter 中不推荐在
scheduleMicrotask()中放置耗时或递归任务?答:因为 Event Loop 在清空 Microtask 队列之前不会处理任何 Event Queue 中的任务。如果 Microtask 队列中有耗时计算或递归任务,会导致 Event Queue 中的 UI 绘制(VSync)和用户触摸点击事件无法被及时处理,从而引发严重的 Flutter 界面冻结和丢帧。
速记
- 优先原则:Microtask 拥有绝对优先权,必须完全清空才轮到 Event。
- 两队列分工:Microtask 放轻量状态同步,Event 放 Future/Timer/UI 事件。
- 卡死避坑:严禁 Microtask 死循环,耗时重载移步 Isolate。