一文学会Dart事件循环及异步调用

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

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

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

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

基本概念

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

事件循环和队列app

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

events going into a queue, feeding into an event loop

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

same figure, but with explicit events: 1. key, 2.click, 3. timer, etc.

你可能在其余的语言中熟悉这些。如今咱们来谈谈dart语言是如何实现的。async

Dart的单线程函数

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

以下图所示,一个Dart程序开始执行的第一步是主isolate执行main()函数,当main()退出后,主isolate线程开始逐个处理程序事件队列上的全部事件。
在这里插入图片描述

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

dart的事件循环和队列

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

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

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

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

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

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

flowchart: main() -&gt; microtasks -&gt; next event -&gt; microtasks -&gt; ...

重要:当事件循环正在执行微任务队列中的任务时,事件队列会卡住:应用程序没法绘制图形、处理鼠标点击、对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()将这个任务添加到微任务队列中。

shows chain of event handler execution, with tasks added using Future and 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 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future #2 of 3
future #3 of 3
future #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 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
future #3 of 4
future #4 of 4
microtask #0 (from future #2b)
future #3a (a new future)
future #3b
future #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 事件循环和异步使用的文章,不过翻译完这篇文章以后没有这个必要了,这篇文章已经将所有的细节描述清楚了)
[英文文章地址]
(https://dart.cn/articles/arch...:~:text=A%20Dart%20app%20has%20a,queue%20and%20the%20microtask%20queue.&text=First,%20it%20executes%20any%20microtasks,item%20on%20the%20event%20queue.)

学习英语对程序员来讲很是必要

相关文章
相关标签/搜索