Dart 中的 Event Loop(事件循环)
了解Dart 中的 Event Loop(事件循环) 及其原理是我们理解 Dart 异步编程的核心。
理解 Dart 的事件循环机制:
同步代码首先执行。
然后,微任务队列(microtask queue)中的所有任务被执行。
最后,事件队列(event queue)中的一个任务被执行,然后再次检查微任务队列,如此循环。
一、核心思想:为什么需要事件循环?
Dart 是单线程的。这意味着它只有一个执行线程,同一时刻只能做一件事。如果让这个线程等待一个耗时的操作(比如网络请求),它就会被“阻塞”,在此期间无法做任何其他事情(比如渲染UI、响应点击),导致应用卡顿。
为了解决这个问题,Dart 没有采用让线程“傻等”的策略,而是使用了基于事件的异步模型。所有潜在的耗时操作(I/O、计时器等)都被“外包”给了操作系统(或其他线程)去执行。Dart 线程本身只负责管理代码执行顺序,它通过一个永不停止的循环——事件循环(Event Loop)——来调度和执行任务。
二、Event Loop 的机制:两个队列(Queue)
事件循环的核心职责是:不断地从两个先进先出(FIFO)的队列中取出任务(Message/Microtask)并执行。
这两个队列是:Microtask Queue(微任务队列) 和 Event Queue(事件队列)
1. Microtask Queue(微任务队列)
优先级最高。
用于存放非常短促、需要尽快异步执行的任务。
通常由 Dart 自身内部产生,例如用于处理
Future
的完成回调、或scheduleMicrotask
函数。在当前事件循环的末尾、在下一个事件被处理之前,会清空整个微任务队列。
2. Event Queue(事件队列)
优先级低于微任务队列。
用于存放外部事件的任务,例如:
I/O 操作(网络请求、文件读写)完成的消息。
用户输入事件(点击、滑动)。
计时器事件(
Timer
、Future.delayed
)。绘制事件。
我们编写的异步代码(如
then
和await
后的代码)最终也大多会被放入事件队列。
三、Event Loop 的工作流程
在Dart中,任务分为两种:事件任务(Event Task)和微任务(Microtask)。
Dart的事件循环会先执行所有微任务队列中的任务,直到微任务队列为空,然后才会从事件任务队列中取出一个任务执行,之后再次检查微任务队列,如此循环。
1. 微任务(Microtask)
微任务通常用于在当前事件循环的末尾、在事件任务开始之前执行一些紧急的工作。
微任务队列比事件任务队列有更高的优先级。
我们可以通过
scheduleMicrotask
函数来添加一个微任务,也可以使用Future.microtask
。微任务包括但不限于:
由
scheduleMicrotask
添加的任务。
Future
对象创建时,通过.then
、.catchError
、.whenComplete
添加的回调,这些回调会被安排为微任务(但注意,不是所有的Future回调都是微任务,有些情况可能会被安排为事件任务,例如使用Future.delayed
)。
(1)特点
优先级高,在当前事件循环的末尾(或下一个事件任务之前)执行,用于需要紧急处理但不阻塞UI的任务。
(2)常见来源
Future
的then
、catchError
、whenComplete
回调Future(() => 42).then((value) => print(value)); // 微任务
scheduleMicrotask
函数scheduleMicrotask(() => print('Microtask'));
Future.microtask
Future.microtask(() => print('Microtask'));
2. 事件任务(Event Task)
事件任务包括I/O、计时器、绘制事件、用户输入事件等。
常见的事件任务有:
Timer(
Future.delayed
内部也是使用Timer)创建的任务。I/O操作完成后的回调。
UI绘制事件(在Flutter中)。
用户输入事件(如点击、滑动等)。
(1)特点
优先级较低,在微任务队列清空后执行,代表异步事件(如I/O、计时器、用户交互)。
(2)常见来源
Future
构造函数Future(() => print('Event Task'));
计时器(
Timer
)Timer(Duration(seconds: 1), () => print('Timer Event'));
I/O 操作(如文件读写、网络请求)
File('path').readAsString().then((content) => print(content));
用户交互事件(在Flutter中)
GestureDetector(onTap: () => print('Tap Event'));
Stream 的监听回调(如
Stream.listen
)
事件循环的运行遵循一个非常严格的顺序,可以用以下伪代码表示:
Dart 事件循环的优先级:同步代码 > 微任务队列 > 事件任务队列。
void eventLoop() {while (true) { // 循环永不停止// 1. 首先,处理所有微任务while (microtaskQueue.isNotEmpty) {final microtask = microtaskQueue.removeFirst();execute(microtask);}// 2. 微任务队列清空后,处理事件队列中的*一个*事件if (eventQueue.isNotEmpty) {final event = eventQueue.removeFirst();execute(event);}// 3. 回到步骤1,检查微任务队列(可能在执行事件时又产生了新的微任务)}
}
关键点:
一个事件循环周期(Turn)只处理事件队列中的一个事件。
但在处理这一个事件之前,必须确保微任务队列是完全空的。
执行事件的过程中,可能会产生新的微任务和事件,它们会被加入到相应的队列中。
示例代码:
void main() {// 微任务scheduleMicrotask(() => print('Microtask 1'));// 事件任务Future(() => print('Event Task 1'));// 另一个微任务Future.microtask(() => print('Microtask 2'));// 另一个事件任务Future.delayed(Duration(seconds: 1), () => print('Event Task 2'));print('Main'); }
输出顺序:
首先打印 'Main',因为它是同步代码。
然后执行微任务队列:先打印 'Microtask 1',然后打印 'Microtask 2'。
接着执行事件任务队列:打印 'Event Task 1'。
最后,在延迟1秒后打印 'Event Task 2'。
注意:虽然
Future.delayed
是一个Future,但它内部使用Timer(事件任务)来延迟执行,所以它的回调是事件任务。总结:
微任务:由
scheduleMicrotask
、Future.microtask
以及一些Future的回调(如.then)添加的任务。事件任务:Timer、I/O、UI事件、用户输入事件等。
四、结合 Future
和 async/await
理解
Future
并不是并行执行的,它只是一个承诺在未来某个时间点返回结果的对象。它的工作流程完美体现了事件循环的机制。
让我们分解一个 Future
的执行过程:
void main() {print('A'); // 同步代码// 创建一个 FutureFuture(() {print('B'); // 异步任务return 100;}).then((value) {// then 回调print('C: $value');});print('D'); // 同步代码
}
执行顺序分析:
main()
函数开始执行。
print('A')
是同步代码,立即执行。输出:A
。遇到
Future(() { print('B'); ... })
。
Future
的构造函数会立即执行,它将传入的匿名函数() { print('B'); ... }
作为一个任务添加到事件队列(Event Queue)中。
Future
对象本身被立即返回。调用
.then(...)
。它注册了一个回调函数((value) { print('C'); }
),这个回调不会立即执行,而是被存储起来,等待Future
完成后触发。
print('D')
是同步代码,立即执行。输出:D
。此时,
main()
函数这个同步代码执行完毕。事件循环开始工作。事件循环检查微任务队列(Microtask Queue),发现为空。
事件循环从事件队列(Event Queue)中取出第一个任务,即我们刚才加入的匿名函数
() { print('B'); ... }
,并执行它。
执行
print('B')
,输出:B
。函数返回
100
,标志着这个Future
完成了。
Future
完成的关键步骤:当Future
完成后,它的then
回调函数不会直接被调用,而是被包装成一个微任务(Microtask),加入到微任务队列的末尾!当前的事件任务(打印B)处理完了。按照规则,事件循环不会立刻去取下一个事件,而是再次检查微任务队列。
它发现微任务队列中有一个新任务(执行
then
回调),于是取出并执行它。
执行
print('C: 100')
,输出:C: 100
。微任务队列再次清空。事件循环这才继续从事件队列中取下一个事件(如果有的话)。
最终输出顺序是:
A
->D
->B
->C: 100
async/await
的本质
async/await
只是Future
和then
的语法糖,它们底层完全依赖相同的事件循环机制。
async
标记的函数会隐式返回一个Future
。
await
关键字会将其后的表达式封装成一个Future
,然后暂停当前async
函数的执行。这个“暂停”并不是阻塞线程,而是立即将线程的控制权交还给事件循环,让它去处理其他任务。
当
await
后的Future
完成后,其后的代码会被包装成一个微任务,加入到微任务队列中,等待执行。所以,从事件循环的视角看,
await
之后的代码和.then()
里的回调没有区别,都是微任务。
五、重要结论与启示
永不阻塞:Dart 线程永远不会被阻塞,它只是在等待事件时变得空闲。真正的 I/O 等工作由操作系统完成,完成后通过事件通知 Dart。
微任务优先:微任务队列的优先级远高于事件队列。滥用微任务(例如使用
scheduleMicrotask
执行长时间操作)会“饿死”事件队列,导致UI无法更新、手势无法响应。执行顺序的可预测性:只要你理解了事件循环的流程,就能准确预测异步代码的执行顺序。同步代码总是最先执行,然后是微任务,最后才是事件。
不要阻塞事件循环:永远不要在事件循环的任务中执行非常耗时的同步代码(如复杂的数学计算、无限循环)。因为这会使事件循环卡在这个任务上,无法处理队列中的其他任务(微任务和事件),导致应用冻结。对于这种CPU密集型任务,必须使用 Isolate。
六、面试题
Q:请写出代码执行结果。
void main() {scheduleMicrotask(() => print('A'));Future(() => print('B')).then(() {print('C');return Future(() => print('D'));}).then(() => print('E'));Future.microtask(() => print('F'));print('G');}
总结输出顺序:
同步代码: G
微任务: A, F
事件任务: B
微任务: C (来自第一个 then 回调)
事件任务: D
微任务: E (来自第二个 then 回调)
所以输出顺序是: G, A, F, B, C, D, E