欢迎关注微信公众号:FSA全栈行动 👋
一、一个能测出水平的面试题
很多 Flutter 开发者天天在写 async/await,感觉上手很顺,但真要是碰到面试官抛出一个关于执行顺序的问题,估计不少人得懵。
假设面试官在白板上给你写下这么一段代码:
Dart
import 'dart:async';
void main() {
print('A');
Future(() => print('B'));
Future.microtask(() => print('C'));
print('D');
}
面试官会问你: "A、B、C、D 的输出顺序是什么?为什么?Event Queue 和 Microtask Queue 有什么区别?scheduleMicrotask 是干嘛的?既然 Dart 是单线程的,它是怎么处理成千上万个并发任务的?"
大多数人的第一直觉可能是 A, B, C, D 或者 A, D, B, C。
但真正的答案是 A, D, C, B。
想要解释清楚这个顺序,你得彻底搞懂 Dart 的异步模型。
二、重新理解 Dart 的单线程模型
Dart 的运行是在一个叫 Isolate 的隔离体中进行的。在同一个 Isolate 内部,它是单线程执行的。所谓的"并发",本质上是依靠 Event Loop(事件循环)来不停地调度任务。
它的工作逻辑其实非常机械化:
-
执行当前的同步代码(Synchronous code)。
-
检查
Microtask Queue,如果里面有东西,一次性全部清空。 -
从
Event Queue中取出一个任务执行。 -
回到步骤 2,循环往复。
这里最关键的认知偏差就在于:Microtask Queue 的优先级高于 Event Queue。
我们可以通过下表直观对比一下这两个队列:
| 维度 | Microtask Queue |
Event Queue |
|---|---|---|
| 优先级 | 极高。在处理下一个事件前,必须清空当前所有微任务 | 常规。每次循环只处理一个事件 |
| 包含内容 | Future.microtask、scheduleMicrotask、.then 回调 |
Future、Timer、Stream 事件、I/O 完成回调 |
| 调度逻辑 | 一旦进入,直到队列为空才停 | 一个一个来,处理完一个会去检查一次微任务 |
三、手把手拆解执行顺序
让我们回到刚才那个面试题,一步步拆解 main 函数的执行过程:
Dart
import 'dart:async';
void main() {
print('A'); // ① 同步代码,立即执行
Future(() => print('B')); // ③ 放入 Event Queue
Future.microtask(() => print('C')); // ② 放入 Microtask Queue
print('D'); // ① 同步代码,立即执行
}
拆解流程如下:
-
main()函数启动,进入同步执行阶段。 -
print('A')执行 → 输出A。 -
Future(() => ...)被调用,B被丢进了Event Queue等着。 -
Future.microtask(() => ...)被调用,C被丢进了Microtask Queue。 -
print('D')执行 → 输出D。 -
同步代码执行完毕,
main()准备返回。 -
事件循环介入 :检查
Microtask Queue,发现C在里面,立即执行 → 输出C。 -
Microtask Queue空了,轮到Event Queue。发现B在里面,执行 → 输出B。
最终输出:A, D, C, B
进阶挑战:嵌套任务的逻辑
如果任务嵌套了,情况会变得更有意思:
Dart
import 'dart:async';
void main() {
print('1 sync');
Future.delayed(Duration.zero, () => print('2 event (delayed)'));
Future.microtask(() {
print('3 microtask');
Future.microtask(() => print('4 nested microtask'));
});
Future(() {
print('5 event');
Future.microtask(() => print('6 microtask inside event'));
});
scheduleMicrotask(() => print('7 scheduleMicrotask'));
print('8 sync end');
}
输出结果:
vbnet
1 sync
8 sync end
3 microtask
7 scheduleMicrotask
4 nested microtask
2 event (delayed)
5 event
6 microtask inside event
这里有几个核心逻辑需要拿捏:
-
同步优先 :
1和8是同步任务,必须先跑完。 -
微任务"死磕到底" :
3执行时又塞进了一个4。注意!4会被排在7后面,但由于Event Loop会一直清空微任务队列,所以3、7、4会在任何Event(如2或5)出现之前全部跑完。 -
Future.delayed(Duration.zero)≠ 微任务 :它本质上是往Event Queue里塞了一个任务,所以它得排在微任务后面。
四、深入 async/await 与 scheduleMicrotask
1. async/await 的本质:状态机
很多人觉得 await 是让线程"停下来等结果",其实不对。Dart 编译 async 函数时,会把它转成一个状态机。
当你 await 一个 Future 时,函数会在这里"挂起",把控制权还给 Event Loop。等 Future 结果出来了,Event Loop 再通过一个微任务把刚才挂起的地方"唤醒"。
这就解释了为什么 await 不会阻塞 UI。
2. scheduleMicrotask 与 Future.microtask
这两者都是往 Microtask Queue 里塞任务,但有个小区别:
-
Future.microtask返回一个Future,你可以用await去等它的结果。 -
scheduleMicrotask返回void,它就是"发射后不管"(fire-and-forget)。
避坑指南: Future.value(x).then(callback) 里的 callback 是直接进 Microtask Queue 的;而普通的 Future(() => ...) 则是先进 Event Queue。
五、实战避坑:别让你的 UI 卡死
既然 Dart 是单线程的,如果我们在主线程(Isolate)里跑一个耗时极长的同步循环,Event Loop 就没法去处理 Timer、I/O 或者 UI 渲染请求了,表现出来的现象就是 Flutter 界面直接卡死(Jank)。
1. 糟糕的写法:阻塞主线程
Dart
int heavySync() {
int sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
return sum;
}
2. 优雅的方案:利用 Isolate
对于纯计算密集型任务,不要在 Isolate 内部死磕,直接开一个新的 Isolate。在现代 Dart 中,推荐使用 Isolate.run(),它非常简洁。
Dart
// ✅ 最优解:直接扔给新的 Isolate
Future<int> heavyInIsolate() async {
return await Isolate.run(() {
int sum = 0;
for (int i = 0; i < 100000000; i++) {
sum += i;
}
return sum;
});
}
3. 微任务饥饿(Microtask Starvation)
这是一种比较隐蔽的 Bug。如果你在微任务里不停地 scheduleMicrotask,导致 Microtask Queue 永远填不满,那么 Event Loop 就会永远卡在"清空微任务"这一步,永远轮不到 Event Queue。
后果: Timer 不触发、Stream 不回调、UI 刷新停滞。
六、总结:面试官到底在考什么
面试官问这些,不是为了让你背出 A, D, C, B,而是看你对 "异步非阻塞" 的底层认知。
记下这些核心点,面试稳过:
-
执行优先级 :同步代码 → 微任务队列(彻底清空) → 事件队列(一个一个来)。
-
单线程并发 :
Dart通过Event Loop实现并发。I/O等耗时操作交给系统底层处理,系统完成后通过Event Queue发送通知,Dart只负责执行回调。 -
async/await本质 :是状态机。await会挂起当前函数并释放控制权,而不是阻塞线程。 -
计算密集型任务 :必须交给
Isolate,否则会由于阻塞Event Loop导致UI掉帧。
一句话口诀: 同步先跑,微任务清空,事件轮转。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~