內容

非同步程式設計通常使用回呼函式,但 Dart 提供了替代方案:FutureStream 物件。Future 就如同對結果的承諾,會在未來的某個時間點提供。Stream 是一種取得一系列值(例如事件)的方式。Future、Stream 等在 dart:async 函式庫中(API 參考)。

dart:async 函式庫同時在網頁應用程式和命令列應用程式中運作。若要使用它,請導入 dart:async

dart
import 'dart:async';

Future

#

Future 物件出現在整個 Dart 函式庫中,通常作為非同步方法傳回的物件。當 future「完成」時,其值就準備好可以使用。

使用 await

#

在直接使用 Future API 之前,請考慮改用 await。使用 await 表達式的程式碼可能比使用 Future API 的程式碼更容易理解。

請考慮以下函式。它使用 Future 的 then() 方法連續執行三個非同步函式,並在執行下一個函式之前等待每個函式完成。

dart
void runUsingFuture() {
  // ...
  findEntryPoint().then((entryPoint) {
    return runExecutable(entryPoint, args);
  }).then(flushThenExit);
}

使用 await 表達式的等效程式碼看起來更像同步程式碼

dart
Future<void> runUsingAsyncAwait() async {
  // ...
  var entryPoint = await findEntryPoint();
  var exitCode = await runExecutable(entryPoint, args);
  await flushThenExit(exitCode);
}

async 函式可以捕捉來自 Future 的例外。例如

dart
var entryPoint = await findEntryPoint();
try {
  var exitCode = await runExecutable(entryPoint, args);
  await flushThenExit(exitCode);
} catch (e) {
  // Handle the error...
}

如需有關如何使用 await 和相關 Dart 語言功能的更多資訊,請參閱 非同步程式設計 codelab

基本用法

#

你可以使用 then() 來排程在未來完成時執行的程式碼。例如,Client.read() 會傳回一個 Future,因為 HTTP 要求可能會花一段時間。使用 then() 讓你可以在 Future 完成且已承諾的字串值可用時執行一些程式碼

dart
httpClient.read(url).then((String result) {
  print(result);
});

使用 catchError() 來處理 Future 物件可能會引發的任何錯誤或例外。

dart
httpClient.read(url).then((String result) {
  print(result);
}).catchError((e) {
  // Handle or ignore the error.
});

then().catchError() 模式是 try-catch 的非同步版本。

串連多個非同步方法

#

then() 方法會傳回一個 Future,提供一種有用的方式來按特定順序執行多個非同步函數。如果向 then() 註冊的回呼傳回一個 Future,then() 會傳回一個 Future,而這個 Future 會以與回呼傳回的 Future 相同的結果完成。如果回呼傳回任何其他類型的值,then() 會建立一個新的 Future,並以該值完成。

dart
Future result = costlyQuery(url);
result
    .then((value) => expensiveWork(value))
    .then((_) => lengthyComputation())
    .then((_) => print('Done!'))
    .catchError((exception) {
  /* Handle exception... */
});

在前面的範例中,這些方法會按以下順序執行

  1. costlyQuery()
  2. expensiveWork()
  3. lengthyComputation()

以下是使用 await 編寫的相同程式碼

dart
try {
  final value = await costlyQuery(url);
  await expensiveWork(value);
  await lengthyComputation();
  print('Done!');
} catch (e) {
  /* Handle exception... */
}

等待多個 future

#

有時你的演算法需要呼叫許多非同步函數,並在繼續之前等待它們全部完成。使用 Future.wait() 靜態方法來管理多個 Future,並等待它們完成

dart
Future<void> deleteLotsOfFiles() async =>  ...
Future<void> copyLotsOfFiles() async =>  ...
Future<void> checksumLotsOfOtherFiles() async =>  ...

await Future.wait([
  deleteLotsOfFiles(),
  copyLotsOfFiles(),
  checksumLotsOfOtherFiles(),
]);
print('Done with all the long steps!');

Future.wait() 會傳回一個 future,在所有提供的 future 完成後完成。它會以它們的結果完成,或是在任何提供的 future 失敗時以錯誤完成。

處理多個 future 的錯誤

#

你也可以等待 iterable 或 future 的 record 上的平行運算。

這些擴充功能會傳回一個 future,其中包含所有提供的 future 的結果值。與 Future.wait 不同,它們也允許你處理錯誤。

如果集合中的任何 future 以錯誤完成,wait 會以 ParallelWaitError 完成。這允許呼叫者處理個別錯誤,並在必要時處置成功的結果。

當你不需要每個個別 future 的結果值時,請對 future 的iterable 使用 wait

dart
void main() async {
  Future<void> delete() async =>  ...
  Future<void> copy() async =>  ...
  Future<void> errorResult() async =>  ...
  
  try {
    // Wait for each future in a list, returns a list of futures:
    var results = await [delete(), copy(), errorResult()].wait;

    } on ParallelWaitError<List<bool?>, List<AsyncError?>> catch (e) {

    print(e.values[0]);    // Prints successful future
    print(e.values[1]);    // Prints successful future
    print(e.values[2]);    // Prints null when the result is an error

    print(e.errors[0]);    // Prints null when the result is successful
    print(e.errors[1]);    // Prints null when the result is successful
    print(e.errors[2]);    // Prints error
  }

}

當你需要每個 future 的個別結果值時,請對 future 的record 使用 wait。這提供了額外的優點,即 future 可以是不同類型

dart
void main() async {
  Future<int> delete() async =>  ...
  Future<String> copy() async =>  ...
  Future<bool> errorResult() async =>  ...

  try {    
    // Wait for each future in a record, returns a record of futures:
    (int, String, bool) result = await (delete(), copy(), errorResult()).wait;
  
  } on ParallelWaitError<(int?, String?, bool?),
      (AsyncError?, AsyncError?, AsyncError?)> catch (e) {
    // ...
    }

  // Do something with the results:
  var deleteInt  = result.$1;
  var copyString = result.$2;
  var errorBool  = result.$3;
}

串流

#

串流物件出現在整個 Dart API 中,代表資料序列。例如,HTML 事件(例如按鈕點擊)是使用串流傳遞的。你也可以將檔案讀取為串流。

使用非同步 for 迴圈

#

有時你可以使用非同步 for 迴圈 (await for) 代替使用串流 API。

考慮以下函數。它使用串流的 listen() 方法來訂閱檔案清單,並傳入一個函數文字,用來搜尋每個檔案或目錄。

dart
void main(List<String> arguments) {
  // ...
  FileSystemEntity.isDirectory(searchPath).then((isDir) {
    if (isDir) {
      final startingDir = Directory(searchPath);
      startingDir.list().listen((entity) {
        if (entity is File) {
          searchFile(entity, searchTerms);
        }
      });
    } else {
      searchFile(File(searchPath), searchTerms);
    }
  });
}

包含非同步 for 迴圈 (await for) 的等效程式碼,看起來更像是同步程式碼

dart
void main(List<String> arguments) async {
  // ...
  if (await FileSystemEntity.isDirectory(searchPath)) {
    final startingDir = Directory(searchPath);
    await for (final entity in startingDir.list()) {
      if (entity is File) {
        searchFile(entity, searchTerms);
      }
    }
  } else {
    searchFile(File(searchPath), searchTerms);
  }
}

如需有關如何使用 await 和相關 Dart 語言功能的更多資訊,請參閱 非同步程式設計 codelab

監聽串流資料

#

若要取得每個值,請使用 await for 或使用 listen() 方法訂閱串流

dart
// Add an event handler to a button.
submitButton.onClick.listen((e) {
  // When the button is clicked, it runs this code.
  submitData();
});

在此範例中,onClick 屬性是由提交按鈕提供的 Stream 物件。

如果你只關心一個事件,你可以使用 firstlastsingle 等屬性來取得它。若要在處理事件之前測試它,請使用 firstWhere()lastWhere()singleWhere() 等方法。

如果你關心事件的子集,你可以使用 skip()skipWhile()take()takeWhile()where() 等方法。

轉換串流資料

#

通常,你需要在使用串流資料之前變更其格式。使用 transform() 方法產生具有不同資料類型的串流

dart
var lines =
    inputStream.transform(utf8.decoder).transform(const LineSplitter());

此範例使用兩個轉換器。首先,它使用 utf8.decoder 將整數串流轉換為字串串流。然後,它使用 LineSplitter 將字串串流轉換為單獨行的串流。這些轉換器來自 dart:convert 函式庫 (請參閱 dart:convert 部分)。

處理錯誤和完成

#

你指定錯誤和完成處理程式碼的方式取決於你使用非同步 for 迴圈 (await for) 或串流 API。

如果你使用非同步 for 迴圈,請使用 try-catch 來處理錯誤。在串流關閉後執行的程式碼會出現在非同步 for 迴圈之後。

dart
Future<void> readFileAwaitFor() async {
  var config = File('config.txt');
  Stream<List<int>> inputStream = config.openRead();

  var lines =
      inputStream.transform(utf8.decoder).transform(const LineSplitter());
  try {
    await for (final line in lines) {
      print('Got ${line.length} characters from stream');
    }
    print('file is now closed');
  } catch (e) {
    print(e);
  }
}

如果你使用串流 API,請透過註冊 onError 監聽器來處理錯誤。透過註冊 onDone 監聽器來執行串流關閉後的程式碼。

dart
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();

inputStream.transform(utf8.decoder).transform(const LineSplitter()).listen(
    (String line) {
  print('Got ${line.length} characters from stream');
}, onDone: () {
  print('file is now closed');
}, onError: (e) {
  print(e);
});

更多資訊

#

有關在命令列應用程式中使用 Future 和 Stream 的一些範例,請查看 dart:io 文件。另請參閱這些文章、程式碼實驗和教學課程