JavaScript中的異步生成器函數[每日前端夜話0xC9]

  • 2019 年 10 月 11 日
  • 筆記

作者:Valeri Karpov

翻譯:瘋狂的技術宅

來源:thecodebarbarian

TC39異步迭代器提案 將 for/await/of 引入了 JavaScript【http://thecodebarbarian.com/getting-started- with-async-iterators-in-node-js】,還介紹了異步生成器函數【https://github.com/tc39/proposal-async-iteration#async-generator-functions】的概念。現在 JavaScript 有 6 種不同的函數類型:

  • 默認函數 function() {}
  • 箭頭函數 () => {}
  • 異步函數 async function() {}
  • 異步箭頭函數 async () => {}
  • 生成器函數 function*() {}
  • 異步生成器函數 async function*() {}

異步生成器函數非常特殊,因為你可以在異步生成器函數中同時使用 awaityield。異步生成器函數與異步函數和生成器函數的不同之處在於,它們不返回 promise 或迭代器,而是返回一個異步迭代器。你可以將異步迭代器視為 iterator,其 next() 函數始終會返回 promise。

你的第一個異步生成器函數

異步生成器函數的行為類似於生成器函數:生成器函數返回一個具有 next() 函數的對象,調用 next() 將執行生成器函數直到下一個 yield。不同之處在於異步迭代器的 next() 函數返回了一個 promise。

下面是帶有異步生成器功能的 「Hello, World」 例子。請注意,以下腳本不適用於 Node.js 10.x 之前的版本。

'use strict';    async function* run() {    await new Promise(resolve => setTimeout(resolve, 100));    yield 'Hello';    console.log('World');  }    // `run()` returns an async iterator.  const asyncIterator = run();    // The function doesn't start running until you call `next()`  asyncIterator.next().    then(obj => console.log(obj.value)). // Prints "Hello"    then(() => asyncIterator.next());  // Prints "World"

遍歷整個異步生成器函數的最乾淨方法是使用 for/await/of 循環。

'use strict';    async function* run() {    await new Promise(resolve => setTimeout(resolve, 100));    yield 'Hello';    console.log('World');  }    const asyncIterator = run();    // Prints "HellonWorld"  (async () => {    for await (const val of asyncIterator) {      console.log(val); // Prints "Hello"    }  })();  

實際用例

你可能會想:「當 JavaScript 已經具有異步功能和生成器功能時,為什麼還需要異步生成器功能?」一個用例是 Ryan Dahl 最初用 Node.js 來解決的經典進度條問題【https://stackoverflow.com/questions/31529013/nodejs-file-upload-with-progress-bar-using-core-nodejs-and-the-original-node-s】。

假設你要循環瀏覽 Mongoose cursor 【https://thecodebarbarian.com/cursors-in-mongoose-45】中的所有文檔,並通過 websocket 或命令行報告進度。

'use strict';    const mongoose = require('mongoose');    async function* run() {    await mongoose.connect('mongodb://localhost:27017/test', { useNewUrlParser: true });    await mongoose.connection.dropDatabase();      const Model = mongoose.model('Test', mongoose.Schema({ name: String }));    for (let i = 0; i < 5; ++i) {      await Model.create({ name: `doc ${i}` });    }      // Suppose you have a lot of documents and you want to report when you process    // each one. You can `yield` after processing each individual doc.    const total = 5;    const cursor = Model.find().cursor();      let processed = 0;    for await (const doc of cursor) {      // You can think of `yield` as reporting "I'm done with one unit of work"      yield { processed: ++processed, total };    }  }    (async () => {    for await (const val of run()) {      // Prints "1 / 5", "2 / 5", "3 / 5", etc.      console.log(`${val.processed} / ${val.total}`);    }  })();

異步生成器函數使你的異步函數可以輕鬆地在 framework-free 【https://www.getrevue.co/profile/masteringjs/issues/framework-free-javascript-why-it-matters-188138】中報告其進度。無需顯式創建 websocket 或登錄控制台 – 如果你的業務邏輯使用 yield 進行進度報告,則可以單獨處理。

Observables

異步迭代器很棒,但是還有另一個並發原語:RxJS observables,異步生成器函數可以很好地與之配合。

'use strict';    const { Observable } = require('rxjs');  const mongoose = require('mongoose');    async function* run() {    // Same as before  }    // Create an observable that emits each value the async generator yields  // to subscribers.  const observable = Observable.create(async (observer) => {    for await (const val of run()) {      observer.next(val);    }  });    // Prints "1 / 5", "2 / 5", "3 / 5", etc.  observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));

使用可觀察的 RxJS 與異步迭代器有兩個主要區別。首先,在上面的示例中,在 subscribe() 中記錄到控制台的代碼是響應式的,而不是命令式的。換句話說,subscribe() handler 無法影響異步函數主體中的代碼,它僅對事件做出反應。例如,使用 for/await/of 循環時,你可以在恢復異步生成器函數之前添加 1 秒的暫停時間。

(async () => {    for await (const val of run()) {      // Prints "1 / 5", "2 / 5", "3 / 5", etc.      console.log(`${val.processed} / ${val.total}`);      // This adds a 1 second delay to every `yield` statement.      await new Promise(resolve => setTimeout(resolve, 1000));    }  })();

第二個是,由於 RxJS 可觀察變量默認情況下是冷操作【https://medium.com/codingthesmartway-com-blog/getting-started-with-rxjs-part-3-hot-and-cold-observables-4713757c9a88】,新的 subscribe() 調用將重新執行該函數。

// Prints "1 / 5", "2 / 5", "3 / 5", etc.  observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));  // Kicks off a separate instance of `run()`  observable.subscribe(val => console.log(`${val.processed} / ${val.total}`));

總結

異步生成器函數乍一看似乎有些小眾並令人困惑,但是它們提供了為 JavaScript 解決進度條問題的本地解決方案。使用 yield 報告異步函數的進度是一個很誘人的想法,因為它使你可以將業務邏輯與進度報告框架分離。下次需要實現進度條時,請試試異步生成器。 原文:http://thecodebarbarian.com/async-generator-functions-in-javascript