一文學會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.)

學習英語對程序員來講很是必要

相關文章
相關標籤/搜索