面试官:几分钟搞懂异步迭代和生成器

2021-03-16 15:29:56 浏览数 (1)

异步的迭代和生成器

异步迭代允许我们迭代异步的、按需的数据。比如,当我们通过网络一块一块地下载东西的时候。而异步生成器使它更加方便。

让我们先看一个简单的示例,以掌握语法,然后回顾一个实际的用例。

回顾迭代

让我们回顾一下关于可迭代对象的话题。我们有一个对象,比如range:

代码语言:javascript复制
let range = {
  from: 1,
  to: 5
};

我们想用for..of循环,例如for(value of range),得到1到5的值。换句话说,我们希望向对象添加迭代功能。可以使用名称Symbol.iterator的特殊方法实现:

  • 当循环开始时,它应该返回一个带有下一个方法的对象。
  • 对于每次迭代,都会为下一个值调用next()方法。
  • next()应该以{done: true/false, value:<循环值>}的形式返回值,其中done:true表示循环结束。

以下是可迭代对象范围的实现:

代码语言:javascript复制
let range = {
  from: 1,
  to: 5,

  [Symbol.iterator]() { // called once, in the beginning of for..of
    return {
      current: this.from,
      last: this.to,

      next() { // called every iteration, to get the next value
        if (this.current <= this.last) {
          return { done: false, value: this.current   };
        } else {
          return { done: true };
        }
      }
    };
  }
};

for(let value of range) {
  alert(value); // 1 then 2, then 3, then 4, then 5
}

如果有任何不清楚的地方,请访问Iterables这一章,它提供了关于常规Iterables的所有细节。

异步迭代

当值异步地出现时,需要异步迭代:在setTimeout或其他类型的延迟之后。

最常见的情况是对象需要发出网络请求来传递下一个值,稍后我们将看到一个真实的例子。

使对象异步可迭代:

  • 使用 Symbol.asyncIterator 代替 Symbol.iterator
  • next()方法应该返回一个承诺(用下一个值来实现)。
  • async关键字处理它,我们可以简单地使用async next()。
  • 要遍历这样一个对象,应该使用for await (let item of iterable)循环。

作为一个开始的例子,让我们创建一个可迭代的range对象,类似于前面的对象,但是现在它将异步返回值,每秒返回一个值。

我们需要做的就是在上面的代码中执行一些替换:

代码语言:javascript复制
let range = {
  from: 1,
  to: 5,

  [Symbol.asyncIterator]() { // (1)
    return {
      current: this.from,
      last: this.to,

      async next() { // (2)

        // note: we can use "await" inside the async next:
        await new Promise(resolve => setTimeout(resolve, 1000)); // (3)

        if (this.current <= this.last) {
          return { done: false, value: this.current   };
        } else {
          return { done: true };
        }
      }
    };
  }
};

(async () => {

  for await (let value of range) { // (4)
    alert(value); // 1,2,3,4,5
  }

})()

正如我们所见,该结构类似于常规迭代器:

  • 要使一个对象异步可迭代,它必须有一个方法Symbol.asyncIterator(1)。
  • 这个方法必须返回一个对象,next()方法返回一个promise(2)。
  • next()方法不一定是异步的,它可以是一个返回promise的常规方法,但异步允许我们使用await,所以这很方便。这里我们稍微延迟一下(3)。
  • 为了进行迭代,我们使用for await(let value of range)(4),即在for后添加await。它调用range[Symbol.asyncIterator]()一次,然后调用next()获取值。

这里是一个小表格,有不同之处:

回顾生成器

现在让我们回顾一下生成器,因为它们可以使迭代代码变得更短。大多数时候,当我们想要创建一个可迭代对象时,我们会使用生成器。

为了简单起见,省略一些重要的东西,它们是“产生(产生)值的函数”。它们将在章节生成器中详细解释。

生成器用函数*(注意星号)标记,并使用yield生成一个值,然后我们可以使用for..把它们循环起来。

这个例子生成了一个从开始到结束的值序列:

代码语言:javascript复制
function* generateSequence(start, end) {
  for (let i = start; i <= end; i  ) {
    yield i;
  }
}

for(let value of generateSequence(1, 5)) {
  alert(value); // 1, then 2, then 3, then 4, then 5
}

我们已经知道,要使对象可迭代,我们应该添加Symbol.iterator

代码语言:javascript复制
let range = {
  from: 1,
  to: 5,
  [Symbol.iterator]() {
    return <object with next to make range iterable>
  }
}

一种常用的Symbol.iterator返回一个生成器,它使代码更短,如你所见:

代码语言:javascript复制
let range = {
  from: 1,
  to: 5,

  *[Symbol.iterator]() { // a shorthand for [Symbol.iterator]: function*()
    for(let value = this.from; value <= this.to; value  ) {
      yield value;
    }
  }
};

for(let value of range) {
  alert(value); // 1, then 2, then 3, then 4, then 5
}

异步生成器

对于大多数实际应用程序,当我们想要创建一个异步生成一系列值的对象时,我们可以使用异步生成器。

语法很简单:将function*async绑定。这使得发电机是异步的。

然后使用for await(…)对其进行迭代,如下所示:

代码语言:javascript复制
async function* generateSequence(start, end) {

  for (let i = start; i <= end; i  ) {

    // Wow, can use await!
    await new Promise(resolve => setTimeout(resolve, 1000));

    yield i;
  }

}

(async () => {

  let generator = generateSequence(1, 5);
  for await (let value of generator) {
    alert(value); // 1, then 2, then 3, then 4, then 5 (with delay between)
  }

})();

由于生成器是异步的,我们可以在其中使用await、依赖 Promises、执行网络请求等等。

异步迭代 range

常规生成器可以用作Symbol.iterator,使迭代代码更短。

与此类似,异步生成器也可以用作Symbol.asyncIterator来实现异步迭代。

代码语言:javascript复制
let range = {
  from: 1,
  to: 5,

  // this line is same as [Symbol.asyncIterator]: async function*() {
  async *[Symbol.asyncIterator]() {
    for(let value = this.from; value <= this.to; value  ) {

      // make a pause between values, wait for something
      await new Promise(resolve => setTimeout(resolve, 1000));

      yield value;
    }
  }
};

(async () => {

  for await (let value of range) {
    alert(value); // 1, then 2, then 3, then 4, then 5
  }

})();

0 人点赞