Javascript 中的异步生成器函数

1,889 阅读4分钟

先来复习几个语法:

  • 函数声明 function() {}
  • 箭头函数 () =>{}
  • 异步函数 async function() {}
  • 异步箭头函数 async () => {}
  • 生成器函数 function*() {}
  • 异步生成器函数 async function*() {}

异步生成器函数很特殊,因为你可以在异步生成器函数中同时使用 await 和 yield。 异步生成器函数与异步函数和生成器函数的不同之处在于它们不返回 promise 或迭代器,而是返回异步迭代器。 你可以将异步迭代器视为迭代器,其next()函数始终返回promise。

写一个异步生成器函数

异步生成器函数的行为与生成器函数类似:生成器函数返回一个具有next()函数的对象,并且调用next()执行生成器函数直到下一个yield。 区别在于异步迭代器的next()函数返回一个promise。

下面是一个带有异步生成器函数的“Hello,World”示例。 请注意,以下脚本不适用于10.x之前的Node.js版本。

'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循环。

pic

'use strict';

async function* run() {
  await new Promise(resolve => setTimeout(resolve, 100));
  yield 'Hello';
  console.log('World');
}

const asyncIterator = run();

// Prints "Hello\nWorld"
(async () => {
  for await (const val of asyncIterator) {
    console.log(val); // Prints "Hello"
  }
})();

小练习

你可能会想“为什么当JavaScript已经具有异步函数和生成器函数时,它需要异步生成器函数?” 一个用例是Ryan Dahl最初编写Node.js来解决的经典进度条问题。

假设你想循环浏览Mongoose游标中的所有文档,并通过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}`);
  }
})();

异步生成器函数使异步函数可以轻松地以无框架方式报告其进度。 无需显式创建websocket或日志到控制台 - 如果假设业务逻辑使用yield来进行进度报告,则可以单独处理。

生成器函数

一般情况下,我们直接称呼: generator 函数,而不是把它翻译过来。generator 本质上也是一个函数,只是它可以像迭代器一样在中间某一步暂停,然后再从暂停处重新开始。简单来说,generator 的表现像是一个具有迭代器行为的函数。

另外一个事实是: async/await 可以基于 generator 实现。

可以看看下面的代码来理解 generator 是什么:

function normalFunc() {
  console.log('I')
  console.log('cannot')
  console.log('be')
  console.log('stopped.')
}

上面的代码如果想在运行时退出,唯一的方法使用 return 或者 throw 抛出一个错误。但是如果你再次调用,它会从顶部重新开始执行。generator 是一种特殊的函数,它简化了迭代任务。generator 会产生一系列返回值而不是一个值。在 Javascript 中,generator 在你调用 next() 的时候返回一个对象。

{
  value: Any,
  done: true|false
}

这里有一张形象的图片:

创建一个 generator

function * generatorFunction() { // Line 1
  console.log('This will be executed first.');
  yield 'Hello, ';   // Line 2
  console.log('I will be printed after the pause');
  yield 'World!';
}
const generatorObject = generatorFunction(); // Line 3
console.log(generatorObject.next().value); // Line 4
console.log(generatorObject.next().value); // Line 5
console.log(generatorObject.next().value); // Line 6
// This will be executed first.
// Hello,
// I will be printed after the pause
// World!
// undefined

我们来逐句解读一下:首先是 function * 。这是一个 generator 的标志。函数体内部没有 return 我们用了 yeild 代替。这个操作符就起到了暂停并且返回的作用。直到下一次调用的时候,从上次 yeild 处从新开始。当然也可以 return ,不过这时候会设置 done 属性为 true。也就是说,当你再次 next 的时候,会得到 done: true 的键值对。

在第三行我们调用了 generator 去生成一个 generator 对象。因为generator 函数总会返回一个 generator 对象,这个对象是可以被迭代的,也就是说它可以放在循环之中。第四行我们调用了 generator 对象上的 next 方法。这个时候 第二行的的 yield 会执行,然后返回一个 { value: 'Hello, ', done: false } 格式接着挂起。当第五行 next 被调用时,首先执行了console.log 接着,返回了 Wolrd。然后generator 继续休眠。第六行我们继续调用。记着如果没有函数显式返回值的话。那么他们会默认返回 undefined。所以,在最后我们得到了 undefined。

最后我们补充一点:async/await 其实就是 generator 的语法糖

const gen = function* () {
  yield "hello";
};


const genAntoerh = async function() {
  await "hello";
}

以上两个函数是等价的。

参考资料