关于node.js:Nodejs-中的异步生成器和异步迭代

31次阅读

共计 5781 个字符,预计需要花费 15 分钟才能阅读完成。

作者:Alan Storm

翻译:疯狂的技术宅

原文:https://alanstorm.com/async-g…

未经容许严禁转载

生成器函数在 JavaScript 中的呈现早于引入 async/await,这意味着在创立异步生成器(始终返回 Promise 且能够 await 的生成器)的同时,还引入了许多须要留神的事项。

明天,咱们将钻研异步生成器及其远亲——异步迭代。

留神 :只管这些概念 应该 实用于所有遵循古代标准的 javascript,但本文中的所有代码都是针对 Node.js 10、12 和 14 版开发和测试的。

异步生成器函数

看一下这个小程序:

// File: main.js
const createGenerator = function*(){
  yield 'a'
  yield 'b'
  yield 'c'
}

const main = () => {const generator = createGenerator()
  for (const item of generator) {console.log(item)
  }
}
main()

这段代码定义了一个生成器函数,用该函数创立了一个生成器对象,而后用 for ... of 循环遍历该生成器对象。相当规范的货色——只管你绝不会在理论工作中用生成器来解决如此琐碎的事件。如果你不相熟生成器和 for ... of 循环,请看《Javascript 生成器》和《ES6 的循环和可迭代对象的》这两篇文章。在应用异步生成器之前,你须要对生成器和 for ... of 循环有扎实的理解。

假如咱们要在生成器函数中应用 await,只有须要用 async 关键字申明函数,Node.js 就反对这个性能。如果你不相熟异步函数,那么请看《在古代 JavaScript 中编写异步工作》一文。

上面批改程序并在生成器中应用 await

// File: main.js
const createGenerator = async function*(){yield await new Promise((r) => r('a'))
  yield 'b'
  yield 'c'
}

const main = () => {const generator = createGenerator()
  for (const item of generator) {console.log(item)
  }
}
main()

同样在理论工作中,你也不会这样做——你可能会 await 来自第三方 API 或库的函数。为了能让大家轻松把握,咱们的例子尽量放弃简略。

如果尝试运行上述程序,则会遇到问题:

$ node main.js
/Users/alanstorm/Desktop/main.js:9
  for (const item of generator) {
                     ^
TypeError: generator is not iterable

JavaScript 通知咱们这个生成器是“不可迭代的”。乍一看,仿佛使生成器函数异步也意味着它生成的生成器是不可迭代的。这有点令人困惑,因为生成器的目标是生成“以编程形式”可迭代的对象。

接下来搞清楚到底产生了什么。

查看生成器

如果你看了 Javascript 生成器这篇文章,那么就应该晓得,如果对象定义了 Symbol.iterator 办法,并且该办法返回,则它在 javascript 中是一个实现了迭代器协定的可迭代对象。当对象具备 next 办法时,该对象将实现迭代器协定,并且该 next 办法返回带有 value 属性,done 属性之一或同时带有 valuedone 属性的对象。

如果用上面这段代码比拟异步生成器函数与惯例生成器函数返回的生成器对象:

// File: test-program.js
const createGenerator = function*(){
  yield 'a'
  yield 'b'
  yield 'c'
}

const createAsyncGenerator = async function*(){yield await new Promise((r) => r('a'))
  yield 'b'
  yield 'c'
}

const main = () => {const generator = createGenerator()
  const asyncGenerator = createAsyncGenerator()

  console.log('generator:',generator[Symbol.iterator])
  console.log('asyncGenerator',asyncGenerator[Symbol.iterator])
}
main()

则会看到,前者 没有 Symbol.iterator 办法,而后者有。

$ node test-program.js
generator: [Function: [Symbol.iterator]]
asyncGenerator undefined

这两个生成器对象 都有 一个 next 办法。如果批改测试代码来调用这个 next 办法:

// File: test-program.js

/* ... */

const main = () => {const generator = createGenerator()
  const asyncGenerator = createAsyncGenerator()

  console.log('generator:',generator.next())
  console.log('asyncGenerator',asyncGenerator.next())
}
main()

则会看到另一个问题:

$ node test-program.js
generator: {value: 'a', done: false}
asyncGenerator Promise {<pending>}

为了使对象可迭代,next 办法须要返回带有 valuedone 属性的对象。一个 async 函数将总是返回一个 Promise 对象。这个个性会带到用异步函数创立的生成器上——这些异步生成器始终会 yield 一个 Promise 对象。

这种行为使得 async 函数的生成器无奈实现 javascript 迭代协定。

异步迭代

侥幸的是有方法解决这个矛盾。如果看一看 async 生成器返回的构造函数或类

// File: test-program.js
/* ... */
const main = () => {const generator = createGenerator()
  const asyncGenerator = createAsyncGenerator()

  console.log('asyncGenerator',asyncGenerator)
}

能够看到它是一个对象,其类型或类或构造函数是 AsyncGenerator 而不是 Generator

asyncGenerator Object [AsyncGenerator] {}

只管该对象有可能不是可迭代的,但它是 异步可迭代 的。

要想使对象可能异步迭代,它必须实现一个 Symbol.asyncIterator 办法。这个办法必须返回一个对象,该对象实现了异步版本的迭代器协定。也就是说,对象必须具备返回 Promisenext 办法,并且这个 promise 必须最终解析为带有 donevalue 属性的对象。

一个 AsyncGenerator 对象满足所有这些条件。

这就留下了一个问题——咱们怎样才能遍历一个不可迭代但能够异步迭代的对象?

for await … of 循环

只用生成器的 next 办法就能够手动迭代异步可迭代对象。(留神,这里的 main 函数当初是 async main ——这样可能使咱们在函数外部应用 await

// File: main.js
const createAsyncGenerator = async function*(){yield await new Promise((r) => r('a'))
  yield 'b'
  yield 'c'
}

const main = async () => {const asyncGenerator = createAsyncGenerator()

  let result = {done:false}
  while(!result.done) {result = await asyncGenerator.next()
    if(result.done) {continue;}
    console.log(result.value)
  }
}
main()

然而,这不是最间接的循环机制。我既不喜爱 while 的循环条件,也不想手动查看 result.done。另外,result.done 变量必须同时存在于外部和内部块的作用域内。

侥幸的是大多数(兴许是所有?)反对异步迭代器的 javascript 实现也都反对非凡的 for await ... of 循环语法。例如:

const createAsyncGenerator = async function*(){yield await new Promise((r) => r('a'))
  yield 'b'
  yield 'c'
}

const main = async () => {const asyncGenerator = createAsyncGenerator()
  for await(const item of asyncGenerator) {console.log(item)
  }
}
main()

如果运行上述代码,则会看到异步生成器与可迭代对象已被胜利循环,并且在循环体中失去了 Promise 的齐全解析值。

$ node main.js
a
b
c

这个 for await ... of 循环更喜爱实现了异步迭代器协定的对象。然而你能够用它遍历任何一种可迭代对象。

for await(const item of [1,2,3]) {console.log(item)
}

当你应用 for await 时,Node.js 将会首先在对象上寻找 Symbol.asyncIterator 办法。如果找不到,它将回退到应用 Symbol.iterator 的办法。

非线性代码执行

await 一样,for await 循环会将非线性代码执行引入程序中。也就是说,你的代码将会以和编写的代码不同的程序运行。

当你的程序第一次遇到 for await 循环时,它将在你的对象上调用 next

该对象将 yield 一个 promise,而后代码的执行将会来到你的 async 函数 ,并且 你的程序将持续在该函数之外执行

一旦你的 promise 失去解决,代码执行将会应用这个值 返回到循环体

当循环完结并进行下一个行程时,Node.js 将在对象上调用 next。该调用会产生另一个 promise,代码执行将会再次来到你的函数。反复这种模式,直到 Promise 解析为 donetrue 的对象,而后在 for await 循环之后继续执行代码。

上面的例子能够阐明一点:

let count = 0
const getCount = () => {
  count++
  return `${count}. `
}

const createAsyncGenerator = async function*() {console.log(getCount() + 'entering createAsyncGenerator')

  console.log(getCount() + 'about to yield a')
  yield await new Promise((r)=>r('a'))

  console.log(getCount() + 're-entering createAsyncGenerator')
  console.log(getCount() + 'about to yield b')
  yield 'b'

  console.log(getCount() + 're-entering createAsyncGenerator')
  console.log(getCount() + 'about to yield c')
  yield 'c'

  console.log(getCount() + 're-entering createAsyncGenerator')
  console.log(getCount() + 'exiting createAsyncGenerator')
}

const main = async () => {console.log(getCount() + 'entering main')

  const asyncGenerator = createAsyncGenerator()
  console.log(getCount() + 'starting for await loop')
  for await(const item of asyncGenerator) {console.log(getCount() + 'entering for await loop')
    console.log(getCount() + item)
    console.log(getCount() + 'exiting for await loop')
  }
  console.log(getCount() + 'done with for await loop')
  console.log(getCount() + 'leaving main')
}

console.log(getCount() + 'before calling main')
main()
console.log(getCount() + 'after calling main')

这段代码你用了编号的日志记录语句,可让你跟踪其执行状况。作为练习,你须要本人运行程序而后查看执行后果是怎么的。

如果你不晓得它的工作形式,就会使程序的执行产生凌乱,但异步迭代确实是一项弱小的技术。


本文首发微信公众号:前端先锋

欢送扫描二维码关注公众号,每天都给你推送陈腐的前端技术文章

欢送持续浏览本专栏其它高赞文章:

  • 深刻了解 Shadow DOM v1
  • 一步步教你用 WebVR 实现虚拟现实游戏
  • 13 个帮你进步开发效率的古代 CSS 框架
  • 疾速上手 BootstrapVue
  • JavaScript 引擎是如何工作的?从调用栈到 Promise 你须要晓得的所有
  • WebSocket 实战:在 Node 和 React 之间进行实时通信
  • 对于 Git 的 20 个面试题
  • 深刻解析 Node.js 的 console.log
  • Node.js 到底是什么?
  • 30 分钟用 Node.js 构建一个 API 服务器
  • Javascript 的对象拷贝
  • 程序员 30 岁前月薪达不到 30K,该何去何从
  • 14 个最好的 JavaScript 数据可视化库
  • 8 个给前端的顶级 VS Code 扩大插件
  • Node.js 多线程齐全指南
  • 把 HTML 转成 PDF 的 4 个计划及实现

  • 更多文章 …

正文完
 0