掌握Dart事件循环与异步调用的全面指南

发表时间: 2020-08-28 10:19

异步代码在Dart中随处可见。许多库函数返回Future对象,您可以注册处理程序来响应事件,如鼠标单击、文件I/O完成和计时。

本文描述了Dart的事件循环架构,您就可以编写出更好的更少问题的异步代码。您将学习如何使用Future,并且能够预测程序的执行顺序

<u>注意:本文中的所有内容既适用于原生运行的Dart应用程序(使用Dart虚拟机),也适用于已经编译成JavaScript的Dart应用程序(dart2js的输出)。本文使用Dart一词来区分Dart应用程序和其他语言编写的软件。</u>

在阅读本文之前,你应该熟悉Future和错误处理的基本知识。

基本概念

如果你写过UI代码,你可能已经熟悉了事件循环和事件队列的概念。它们确保了图形操作和事件(如鼠标点击)一次只处理一个。

事件循环和队列

事件循环的工作是从事件队列中获取一个事件并处理它,只要队列中有事件,就重复这两个步骤。

队列中的事件可能代表用户输入,文件I / O通知,计时器等。 例如,下面是事件队列的图片,其中包含计时器和用户输入事件:

你可能在其他的语言中熟悉这些。现在我们来谈谈dart语言是如何实现的。

Dart的单线程

一旦一个Dart函数开始执行,它将继续执行直到退出。换句话说,Dart函数不能被其他Dart代码打断。

如下图所示,一个Dart程序开始执行的第一步是主isolate执行main()函数,当main()退出后,主isolate线程开始逐个处理程序事件队列上的所有事件。

实际上,这有点过于简化了。

dart的事件循环和队列

Dart应用程序的事件循环带有两个队列——事件队列和微任务队列。

事件队列包含所有外部事件:I/O、鼠标事件、绘图事件、计时器、Dart isolate之间的通信,等等。

微任务队列是必要的,因为事件处理代码有时需要稍后完成一个任务,但在将控制权返回到事件循环之前。例如,当一个可观察对象发生变化时,它将几个突变变化组合在一起,并同步地报告它们。微任务队列允许可观察对象在DOM显示不一致状态之前报告这些突变变化。

事件队列包含来自Dart和系统中其他的事件,微任务队列只包含来自Dart核心代码的事件。

如下图所示,当main()函数退出时,事件循环开始工作。首先,它以FIFO(先进先出)顺序执行所有微任务。然后,它使事件队列中的第一项出队并处理,然后它重复这个循环:执行所有微任务,然后处理事件队列上的下一事件。一旦两个队列都为空并且不会再发生任何事件,应用程序的嵌入程序(如浏览器或测试框架)就可以释放应用程序。

<u>注意:如果web应用程序的用户关闭了它的窗口,那么web应用程序可能会在其事件队列为空之前强行退出。</u>

重要:当事件循环正在执行微任务队列中的任务时,事件队列会卡住:应用程序无法绘制图形、处理鼠标点击、对I/O做出反应等。

尽管可以预测任务执行的顺序,但不能准确预测事件循环何时将任务从队列中移除。Dart事件处理系统基于单线程循环;它不是基于任何类型的时间标准。例如,当您创建一个延迟的任务时,事件将在您指定的时间进入队列。他还是要等待事件队列中它之前的所有事件(包括微任务队列中的每一个事件)全部执行完后,才能得到执行。(延时任务不是插队,是在指定时间进入队列)

提示:链式调用future指定任务顺序

如果您的代码有依赖关系,请以显式的方式编写。显式依赖关系帮助其他开发人员理解您的代码,并且使您的程序更能抵抗代码重构。

下面是一个错误编码方式的例子:

// 因为在设置变量和使用变量之间没有明确的依赖关系,所以不好。future.then((){...设置一个重要变量...)。Timer.run(() {...使用重要变量...})。

相反,像这样写代码:

//更好,因为依赖关系是显式的。

future.then(…设置一个重要的变量…)then((_){…使用重要的变量…});

在使用该变量之前必须先设置它。(如果您希望即使出现错误也能执行代码,那么可以使用whenComplete()而不是then()。)

如果使用变量需要时间并且可以在以后完成,请考虑将代码放在新的Future中:

//可能更好:显式依赖加上延迟执行。future.then(…设置一个重要的变量…)then((_) {new Future((){…使用重要的变量…})});

使用新的Future使事件循环有机会处理事件队列中的其他事件。下一节将详细介绍延迟运行的调度代码。

如何安排任务

当您需要指定一些需要延迟执行的代码时,可以使用dart:async库提供的以下API:

Future类,它将一个项目添加到事件队列的末尾。

顶级的scheduleMicrotask()函数,它将一个项目添加到微任务队列的末尾。

使用这些api的示例在下一节中。事件队列:new Future()和微任务队列:scheduleMicrotask()

使用适当的队列(通常是事件队列)

尽可能的在事件队列上调度任务,使用Future。使用事件队列有助于保持微任务队列较短,减少微任务队列影响事件队列的可能。

如果一个任务需要在处理任何来自事件队列的事件之前完成,那么你通常应该先执行该函数。如果不能先执行,那么使用 scheduleMicrotask()将这个任务添加到微任务队列中。

事件队列: new Future()

要在事件队列上调度任务,可以使用new Future()或new Future.delayed()。这是dart:async库中定义的两个Future的构造函数

注意:您也可以使用Timer安排任务,但是如果Timer任务中发生任何未捕获的异常,您的应用程序将退出。 相反,我们建议使用Future,它建立在Timer之上,并增加了诸如检测任务完成和对错误进行响应的功能。

要立即将一个事件放到事件队列中,使用new Future():

//在事件队列中添加任务。new Future((){ /……代码就在这里……});

您可以添加对then()或whenComplete()的调用,以便在新的Future完成后立即执行一些代码。例如,当new Future的任务离开队列时,以下代码输出“42”:

new Future(() => 21)    .then((v) => v*2)    .then((v) => print(v));

使用new Future.delayed()在一段时间后在队列中加入一个事件:

// 一段时间之后,将事件加入队列new Future.delayed(const Duration(seconds:1), () {  // ...代码在这里...});

尽管前面的示例在一秒后将任务添加到事件队列中,但该任务只有在主isolate空闲、微任务队列为空以及之前在事件队列中入队的任务全部执行完后才能执行。例如,如果main()函数或事件处理程序正在运行一个复杂的计算,则任务只有在该计算完成后才能执行。在这种情况下,延迟可能远不止一秒。

关于future的重要细节:

1 传递给Future的then()方法的函数在Future完成时立即执行。(函数没有进入队列,只是被调用了)

2 如果Future在调用then()之前已经完成,则将一个任务添加到微任务队列,然后该任务执行传递给then()的函数。

3 Future()和Future.delayed()构造函数不会立即完成; 他们将一个项目添加到事件队列。

4 value()构造函数在微任务中完成,类似于#2

5 Future.sync()构造函数立即执行其函数参数,并且(除非该函数返回Future,如果返回future代码会进入事件队列)在微任务中完成,类似于#2。(Future.sync(FutureOr<T> computation())该函数接受一个function参数)

微任务队列:scheduleMicrotask()

async库将scheduleMicrotask()定义为一个顶级函数。你可以像这样调用scheduleMicrotask():

scheduleMicrotask(() {  // ...代码在这里...});

由于bug 9001和9002,第一次调用scheduleMicrotask()会将一个创建微任务队列的事件放在事件队列中;此事件创建微任务队列,并将指定给scheduleMicrotask()的函数放入微任务队列,只要微任务队列至少有一个事件,后续对 scheduleMicrotask() 的调用就会正确地添加到微任务队列中。一旦微任务队列为空,下次调用 scheduleMicrotask()时必须重新创建(意味着第一次调用scheduleMicrotask()不会直接进入微任务队列立即执行,会在事件队列上先插入一个创建微任务队列的事件,这个事件还是要在事件队列中排队)。

这些错误的结果是:使用scheduleMicrotask()调度的第一个任务似乎位于事件队列上。

(译者注:dart2.9会将第一次调用scheduleMicrotask()时,将此代码插入事件队列的第一位)

向微任务队列添加任务的一种方法是在已经完成的Future上调用then()。有关更多信息,请参阅前一节(关于future的重要)

必要时使用isolates 或workers

现在您已经阅读了关于调度任务的所有内容,让我们测试一下您的理解。

请记住,您不应该依赖Dart的事件队列实现来指定任务顺序。 实现可能会发生变化,Future的then()和whenComplete()方法是更好的选择。 不过,如果您能正确回答下面这些问题,你学会了。

练习

Question #1

这个示例打印出什么?

import 'dart:async';void main() {  print('main #1 of 2');  scheduleMicrotask(() => print('microtask #1 of 2'));  new Future.delayed(new Duration(seconds:1),                     () => print('future #1 (delayed)'));  new Future(() => print('future #2 of 3'));  new Future(() => print('future #3 of 3'));  scheduleMicrotask(() => print('microtask #2 of 2'));  print('main #2 of 2');}

答案

main #1 of 2main #2 of 2microtask #1 of 2microtask #2 of 2future #2 of 3future #3 of 3future #1 (delayed)

这个顺序应该你能预料到的,因为示例代码分三批执行:

1 main()函数中的代码

2 微任务队列中的任务(scheduleMicrotask())

3 事件队列中的任务(new Future()或new Future.delayed())

请记住,main()函数中的所有调用都是从头到尾同步执行的。首先main()调用print(),然后调用scheduleMicrotask(),再调用new Future.delayed(),然后调用new Future(),以此类推。只有回调--作为 scheduleMicrotask()、new Future.delayed()和new Future()的参数代码才会在后面的时间执行。

注意:目前,如果注释掉对scheduleMicrotask()的第一个调用,那么对#2和#3的回调将在微任务#2之前执行。这是由于bug 9001和9002造成的,如微任务队列: scheduleMicrotask()中所述。

Question #2

这里有一个更复杂的例子。如果您能够正确地预测这段代码的输出,就会得到一个闪亮的星星。

import 'dart:async';void main() {  print('main #1 of 2');  scheduleMicrotask(() => print('microtask #1 of 3'));  new Future.delayed(new Duration(seconds:1),      () => print('future #1 (delayed)'));  new Future(() => print('future #2 of 4'))      .then((_) => print('future #2a'))      .then((_) {        print('future #2b');        scheduleMicrotask(() => print('microtask #0 (from future #2b)'));      })      .then((_) => print('future #2c'));  scheduleMicrotask(() => print('microtask #2 of 3'));  new Future(() => print('future #3 of 4'))      .then((_) => new Future(                   () => print('future #3a (a new future)')))      .then((_) => print('future #3b'));  new Future(() => print('future #4 of 4'));  scheduleMicrotask(() => print('microtask #3 of 3'));  print('main #2 of 2');}

假设错误9001/9002没有修复,输出如下:

main #1 of 2main #2 of 2microtask #1 of 3microtask #2 of 3microtask #3 of 3future #2 of 4future #2afuture #2bfuture #2cfuture #3 of 4future #4 of 4microtask #0 (from future #2b)future #3a (a new future)future #3bfuture #1 (delayed)

(译者注)

这是译者在dart2.9上运行的结果。dart程序会在第一次创建微任务队列时,将创建微任务队列的代码插入到事件队列的第一位,相当于插队。

原作者说的bug已经修复了

总结

你现在应该了解Dart的事件循环以及dart如何安排任务。以下是Dart中事件循环的一些主要概念:

Dart应用程序的事件循环使用两个队列执行任务:事件队列和微任务队列。

事件队列有来自Dart(futures、计时器、isolate messages)和系统(用户操作、I/O等)的事件。

目前,微任务队列只有来自Dart核心代码的事件,如果你想让你的代码进入微任务队列执行,使用scheduleMicrotask()。

事件循环在退出队列并处理事件队列上的下一项之前先清空微任务队列。

一旦两个队列都为空,应用程序就完成了它的工作,并且(取决于它的嵌入程序)可以退出。

main()函数和来自微任务和事件队列的所有项目都运行在Dart应用程序的主isolates 上。

当你安排一项事件时,遵循以下规则:

如果可能,将其放在事件队列中(使用new Future()或new Future.delayed())。

使用Future的then()或whenComplete()方法指定任务顺序。

为了避免耗尽事件循环,请保持微任务队列尽可能短。

为了保持应用程序的响应性,避免在任何一个事件循环中执行计算密集型任务。

要执行计算密集型任务,请创建额外的isolates 或者 workers。

(译者本来想自己总结一篇dart 事件循环和异步使用的文章,不过翻译完这篇文章之后没有这个必要了,这篇文章已经将全部的细节描述清楚了)