Dart回调调度顺序规则与HttpRequest回调穿插实现方法
问题1:现象的根本原因
这个现象是Dart事件循环的双队列优先级调度规则导致的:
- Dart事件循环维护两个任务队列:微任务队列(Microtask Queue) 和 事件队列(Event Queue),调度优先级固定为:微任务队列 > 事件队列。
- 每轮事件循环的执行逻辑是:
- 先执行完当前微任务队列中所有待执行任务,执行过程中新加入微任务队列的任务也会在本轮被依次处理,直到微任务队列完全清空
- 从事件队列中取出1个队首任务执行
- 回到第一步重复循环
你的代码里两个关键点刚好触发了这个调度规则:
incr是无await的async函数,它返回的Future对应的then回调,会在Future完成时被加入微任务队列。_tick和_tickB的递归逻辑,会在每一轮执行完后立刻往微任务队列末尾追加下一轮的回调,只要递归没结束(i没到阈值),微任务队列就永远不为空。HttpRequest.getString的网络IO完成事件属于事件队列任务,只要微任务队列一直有等待执行的任务,事件队列的所有任务都得不到调度机会。因此无论网络请求多早返回,layout received的打印必然要等两组tick的所有回调全部执行完才会触发,自然不会出现穿插。
问题2:让回调穿插执行的调度方法
核心思路是:不要让轮询逻辑持续占满微任务队列,主动给事件队列让出执行窗口,常见实现方式有两种:
方法1:将轮询的递归调度切换到事件队列
默认的Future默认构造函数、Timer.run、Future.delayed(Duration.zero)都会把任务调度到事件队列,替换原来基于微任务的调度即可。
示例修改:
// 将incr的任务调度从微任务改为事件队列 Future<int> incr(int i) { return Future(() { i++; return i; }); }
修改后,每一轮tick执行完,下一轮incr任务会被放到事件队列队尾,这时候微任务队列会被清空,事件循环就会按入队顺序依次处理事件队列中的任务:如果网络请求已经返回,对应的回调就会在轮询间隙被执行,实现穿插效果。
方法2:轮询中主动让出调度权
如果不想修改原有incr逻辑,可以在每轮递归前显式插入一个空的事件队列任务,强制让出调度权:
void _tick(int i) async { print("A $i"); if (i < 10) { // 插入空的事件队列任务,让出调度窗口 await Future.delayed(Duration.zero); incr(i).then(_tick); } } void _tickB(int i) async { print("B $i"); if (i < 10) { await Future.delayed(Duration.zero); incr(i).then(_tickB); } }
这种方式的效果和方法1一致,每执行一轮打印就会让出一次事件循环,网络IO的回调就有机会在轮询间隙被处理。
注意:如果轮询逻辑持续占用微任务队列,本质和同步阻塞类似,会导致所有IO、计时器、用户交互相关的事件队列任务全部被延迟,生产环境要避免这种写法。
内容的提问来源于stack exchange,提问作者Preston
相关产品推荐
相关产品推荐

