引言
前两天在整理 Flutter 面试题时,遇到这么一道经典题:
dart
Future<void> main() async {
print('1');
Future.microtask(() => print('2'));
Future(() => print('3'));
await Future(() => print('4'));
print('5');
Future.microtask(() => print('6'));
print('7');
}
问:输出顺序是什么?
很多人第一反应是 1, 2, 3, 4, 5, 6, 7,也有人猜 1, 3, 2, 4, 5, 7, 6。但真正的答案是:
1
2
3
4
5
7
6
为什么是这个顺序?为什么 7 在 6 前面?这背后藏着 Dart 事件循环(Event Loop)的核心调度哲学。本文就从这道题出发,把 Dart 的事件循环机制彻底讲透。
一、为什么需要事件循环?
在讲机制之前,先想一个问题:Dart 是单线程语言,它怎么处理异步任务?
iOS 开发者可能会想到 RunLoop,前端同学会想到浏览器的 Event Loop,Java 开发者会想到 Looper。没错,单线程语言处理异步的核心思想都是事件循环。
Dart 的事件循环做的事情很简单:
scss
while (true) {
// 1. 先把 microtask 队列清空
while (microtaskQueue.isNotEmpty) {
microtaskQueue.removeFirst().run();
}
// 2. 再处理一个 event
if (eventQueue.isNotEmpty) {
eventQueue.removeFirst().run();
}
}
注意两个关键点:
- microtask 队列优先级更高,每轮循环都要清空
- event 队列每轮只取一个处理,处理完后回到第一步重新检查 microtask
二、两个队列:Microtask vs Event
| 特性 | Microtask Queue | Event Queue |
|---|---|---|
| 优先级 | 高(每轮清空) | 低(每轮取 1 个) |
| 来源 | Future.microtask()、scheduleMicrotask()、await 的 continuation |
Future()、Timer、I/O、手势事件 |
| 设计目的 | 短任务,需要在当前事件之后立即执行 | 常规异步任务、I/O、定时器 |
类比 iOS 生态
| Dart | iOS 对标 |
|---|---|
| Event Loop | CFRunLoop |
| Microtask Queue | kCFRunLoopBeforeSources 阶段的 perform block |
| Event Queue | Source0 / Source1 事件源 |
await continuation |
dispatch_async 到主队列的 block |
三、逐行拆解面试题
我们把执行过程分成三个阶段来看。
阶段 1:main() 同步执行
dart
print('1'); // 同步输出 1
Future.microtask(() => print('2')); // 加入 microtask 队列
Future(() => print('3')); // 加入 event 队列
await Future(() => print('4')); // 加入 event 队列,main 挂起
执行到 await 时,main 函数挂起,把控制权交还给事件循环。
此时队列状态:
- microtask 队列 :
[print2] - event 队列 :
[print3, print4]
阶段 2:事件循环接管
第 1 轮循环:
csharp
① 清空 microtask → 执行 print2 → 输出 2
② 取一个 event → 执行 print3 → 输出 3
第 2 轮循环:
csharp
① microtask 空,跳过
② 取一个 event → 执行 print4 → 输出 4
→ Future4 完成,await 的 continuation 加入 microtask
第 3 轮循环:
scss
① 清空 microtask → 执行 continuation(恢复 main 函数)
├─ print('5') → 输出 5
├─ Future.microtask(print6) → 加入 microtask 队列
└─ print('7') → 输出 7
microtask 队列还有 print6,继续执行 → 输出 6
这就解释了为什么 7 在 6 前面:print6 是在 continuation 执行期间才被加入 microtask 队列的,要等当前 microtask(也就是 print5~print7 这段)执行完才能轮到它。
四、流程图直观展示

五、await 的本质
很多人把 await 理解成"等待",但更准确的理解是:挂起当前函数,把后续代码包装成一个 microtask,然后让出控制权。
dart
// 这段代码
Future<void> foo() async {
await bar();
doSomething();
}
// 大致等价于
Future<void> foo() {
return bar().then((_) {
doSomething();
});
}
then 注册的回调,本质上就是一个 microtask。所以 await 之后的代码,会在 Future 完成后的下一轮 microtask 清空阶段执行。
六、在 Flutter 中的实际意义
理解事件循环,能帮我们解决很多实际问题:
1. 为什么 setState 后 UI 不会立即更新?
dart
setState(() {
_count = 1;
});
print(_count); // 这里 _count 已经是 1 了,但 UI 还没刷新
setState 只是标记了"需要重建",真正的重建是在当前事件循环结束后的下一帧 (由 VSync 信号驱动的 event)。所以在同一个 microtask 或同步代码中,多次 setState 只会触发一次重建。
2. 为什么 microtask 不能滥用?
dart
// 危险写法:大量 microtask 会阻塞 event 处理
for (var i = 0; i < 10000; i++) {
Future.microtask(() => heavyCompute());
}
因为每轮事件循环要清空所有 microtask 才会处理 event,如果你往 microtask 队列里塞了大量耗时任务,event 队列里的手势、绘制、定时器都会被饿死,最终表现就是掉帧、无响应。
3. 如何安全地"插队"执行?
dart
// 场景:在 build 完成后做一些事情
WidgetsBinding.instance.addPostFrameCallback((_) {
// 这个回调是在帧绘制完成后触发的 event
// 不会阻塞当前帧的渲染
});
addPostFrameCallback 用的是 event 队列而非 microtask,所以不会影响当前帧。
七、总结
回到那道面试题,记住三句话就能秒杀:
- 同步代码先行:所有同步的 print 先执行完
- microtask 插队:每轮事件循环先清空 microtask,再碰 event
- await 让出 + 回调入 microtask:await 之后的代码要等 Future 完成后,通过 microtask 恢复执行
最终输出:
csharp
1 → 同步
2 → microtask(第 1 轮清空)
3 → event(第 1 轮取 1 个)
4 → event(第 2 轮取 1 个),完成后 continuation 入 microtask
5 → continuation 中同步执行
7 → continuation 中同步执行
6 → 第 3 轮继续清空 microtask
写在最后
事件循环是 Dart 异步编程的基石,理解它不只是为了应付面试,更是为了写出高性能、不卡顿的 Flutter 应用。当你遇到"为什么我的 UI 不刷新"、"为什么这个回调没执行"、"为什么页面掉帧"时,回到事件循环的模型去思考,往往能找到答案。
如果你觉得这篇文章对你有帮助,欢迎点赞 👍 收藏 ⭐,有问题也欢迎在评论区交流~
参考资料: