用图表学习掌握 异步/同步知识

web前端开发

共 8919字,需浏览 18分钟

 ·

2022-07-05 06:35

英文 | https://medium.com/frontend-canteen/you-can-master-async-await-with-7-diagrams-ac96a97abe92

翻译 | 杨小爱


您可能已经阅读了一些关于 异步/同步 的文章,甚至使用它们编写了一些代码。但是你真的掌握了异步/同步吗?

在本文中,让我们讨论以下主题:

  • 异步/同步的基本用法。

  • 然后我们了解异步的祖先,生成器函数。

  • 最后,让我们自己实现 异步/同步

我准备了 7 个图表来解释这些概念,希望它们能帮助您更轻松地理解这些主题。

异步/同步的基

一句话总结异步/同步的用法就是:以同步的方式进行异步操作。

比如有这样一个场景:我们需要请求一个API,收到响应后,再请求另一个API。

然后我们可以这样写代码:

function request(num) { // mock HTTP request  return new Promise(resolve => {    setTimeout(() => {      resolve(num * 2)    }, 1000)  })}
request(1).then(res1 => { console.log(res1) // it will print `2` after 1 second
request(2).then(res2 => { console.log(res2) // it will print `4` after anther 1 second })})

或者还有另外一种场景:我们需要请求一个API,收到响应后,再以之前的响应作为参数请求另一个API。

然后我们可以这样写代码:

request(5).then(res1 => {  console.log(res1) // it will print `10` after 1 second
request(res1).then(res2 => { console.log(res2) // it will print `20` after anther 1 second })})

上面两段代码确实可以解决问题,但是如果嵌套层级太多,代码就会不美观、不可读。

解决这个问题的方法是使用异步/同步。它允许我们以同步的方式执行异步操作。

异步/同步重构以上两段代码后,它们看起来像这样:

示例 1:

async function fn () {  await request(1)  await request(2)}fn()

示例 2:

async function fn () {const res1 = await request(5)const res2 = await request(res1)console.log(res2)}fn()

JavaScript 引擎会等待 await 关键字之后的表达式的结果返回,然后再继续执行下面的代码。

以上代码执行流程示意图:

就像你在加油站加油一样,只有当前一辆车加满油后,才能轮到下一辆车加油。在async函数中,await指定异步操作只能在队列中一个一个执行,从而达到以同步方式执行异步操作的效果。

注意:await 关键字只能用在 async 函数中,否则会报错。

那我们要知道await后面不能跟普通函数,否则就达不到排队的效果。

下面的代码是一个不正确的例子。

function request(num) {  setTimeout(() => {    console.log(num * 2)  }, 1000)}
async function fn() { await request(1) // 2 await request(2) // 4 // print `2` and `4` at the same time}fn()

生成器函数

async/await 本身的用法很简单,但它实际上是一种语法糖。async/await 是 ES2017 中引入的一种语法。如果你尝试将async/await语法的代码编译到ES2015版本,你会发现它们会被编译成generate函数,所以这里我们先了解generate函数。

生成器函数是使用 function* 语法编写的。调用时,生成器函数最初不会执行它们的代码。相反,它们返回一种特殊类型的迭代器,称为生成器。当调用生成器的 next 方法消耗了一个值时,生成器函数会一直执行,直到遇到 yield 关键字。

这是一个例子:

function* gen() {  yield 1  yield 2  yield 3}const g = gen()console.log(g.next()) // { value: 1, done: false }console.log(g.next()) // { value: 2, done: false }console.log(g.next()) // { value: 3, done: false }console.log(g.next()) // { value: undefined, done: true }

上面代码中,gen函数没有返回值,所以最后一次调用g.next()返回的结果的value属性是未定义的。

如果 generate 函数有返回值,那么最后一次调用 g.next() 返回的结果的 value 属性就是结果。

function* gen() {  yield 1  yield 2  yield 3  return 4}const g = gen()console.log(g.next()) // { value: 1, done: false }console.log(g.next()) // { value: 2, done: false }console.log(g.next()) // { value: 3, done: false }console.log(g.next()) // { value: 4, done: true }

如果我们用一张图来表示上述函数的执行,它应该是这样的:

yield a function

如果yield后面跟着函数调用,那么这里程序执行完之后,会立即调用函数。并且函数的返回值会放在 g.next() 的结果的 value 属性中。

function fn(num) {  console.log(num)  return num}function* gen() {  yield fn(1)  yield fn(2)  return 3}const g = gen()console.log(g.next()) // 1// { value: 1, done: false }console.log(g.next())// 2//  { value: 2, done: false }console.log(g.next()) // { value: 3, done: true }

Promise 

同样,Promise 对象也可以放在 yield 之后。那么程序的执行流程和之前一样。

function fn(num) {  return new Promise(resolve => {    setTimeout(() => {      resolve(num)    }, 1000)  })}function* gen() {  yield fn(1)  yield fn(2)  return 3}const g = gen()console.log(g.next()) // { value: Promise { <pending> }, done: false }console.log(g.next()) // { value: Promise { <pending> }, done: false }console.log(g.next()) // { value: 3, done: true }

此代码的执行流程示意图:

但是,我们要的不是处于pending状态的Promise对象,而是Promise完成后存储在其中的值。那么我们如何修改上面的代码呢?

很简单,我们只需要调用 .then 方法:

const g = gen()const next1 = g.next()next1.value.then(res1 => {  console.log(next1) // print { value: Promise { 1 }, done: false } after 1 second  console.log(res1) // print `1` after 1 second
const next2 = g.next() next2.value.then(res2 => { console.log(next2) // print { value: Promise { 2 }, done: false } after 2 seconds console.log(res2) // print `2` after 2 seconds console.log(g.next()) // print { value: 3, done: true } after 2 seconds })})

以上代码执行流程示意图:

在 next() 中传递一个参数

然后,在调用 next() 函数时,我们可以传递参数。

function* gen() {  const num1 = yield 1  console.log(num1)  const num2 = yield 2  console.log(num2)  return 3}const g = gen()console.log(g.next()) // { value: 1, done: false }console.log(g.next(11111))// 11111//  { value: 2, done: false }console.log(g.next(22222)) // 22222// { value: 3, done: true }

这里需要注意的是,第一次调用next()方法时,传参是没有作用的。

每次调用 g.next() 时,返回的结果都与我们之前的情况没有什么不同。而num1会接受g.next(11111)的参数11111,num2会接受g.next(11111)的参数22222。

此代码的执行流程示意图:

Promise + Pass param

之前我们提到过Promise对象可以放在yield之后,我们也提到过可以在next函数中传入参数。

如果我们将这两个功能放在一起,它会变成这样:

function fn(nums) {  return new Promise(resolve => {    setTimeout(() => {      resolve(nums * 2)    }, 1000)  })}function* gen() {  const num1 = yield fn(1)  const num2 = yield fn(num1)  const num3 = yield fn(num2)  return num3}const g = gen()const next1 = g.next()next1.value.then(res1 => {  console.log(next1) // print { value: Promise { 2 }, done: false } after 1 second  console.log(res1) // print `2` after 1 senond
const next2 = g.next(res1) // pass privouse result next2.value.then(res2 => { console.log(next2) // print { value: Promise { 4 }, done: false } after 2 seconds console.log(res2) // print `4` after 2 senond
const next3 = g.next(res2) // pass privouse result `res2` next3.value.then(res3 => { console.log(next3) // print { value: Promise { 8 }, done: false } after 3 seconds console.log(res3) // print `8` after 3 senond
// pass privouse result `res3` console.log(g.next(res3)) // print { value: 8, done: true } after 3 seconds }) })})

其实上面的写法和async/await很像。

唯一的区别是:

  • gen函数执行后,返回值不是Promise对象。但是 asyncFn 的返回值是 Promise

  • gen函数需要执行特定的操作才相当于asyncFn的排队效果

  • gen函数执行的操作是不完善的,它规定只能处理三层嵌套

下面我们将解决这些问题并自己实现 async/await。

现async/await

为了解决前面提到的问题,我们可以封装一个高阶函数。这个高阶函数可以接受一个生成器函数,经过一系列的处理,返回一个新的函数,工作起来就像一个真正的异步函数。

function generatorToAsync(generatorFn) {  // do something  return `a function works like a real async function`}

异步函数的返回值应该是一个 Promise 对象,所以我们的 generatorToAsync 函数的模板应该是这样的:

function* gen() {
}function generatorToAsync (generatorFn) { return function () { return new Promise((resolve, reject) => {
}) }}
const asyncFn = generatorToAsync(gen)
console.log(asyncFn()) // an Promise object

然后,我们可以将前面的代码复制到 generatorToAsync 函数中:

function fn(nums) {  return new Promise(resolve => {    setTimeout(() => {      resolve(nums * 2)    }, 1000)  })}function* gen() {  const num1 = yield fn(1)  const num2 = yield fn(num1)  const num3 = yield fn(num2)  return num3}function generatorToAsync(generatorFn) {  return function () {    return new Promise((resolve, reject) => {      const g = generatorFn()      const next1 = g.next()      next1.value.then(res1 => {
const next2 = g.next(res1) next2.value.then(res2 => {
const next3 = g.next(res2) next3.value.then(res3 => {
resolve(g.next(res3).value) }) }) }) }) }}
const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res))

但是,上面的代码只能处理三个yield,而在实际项目中,yield的个数是不确定的,可能是3、5或10。所以我们还需要调整代码,让我们的generatorToAsync函数可以处理任何 产量数:

function generatorToAsync(generatorFn) {  return function() {    const gen = generatorFn.apply(this, arguments) // there may be arguments of gen function
// return a Promise object return new Promise((resolve, reject) => {
function go(key, arg) { let res try { res = gen[key](arg) } catch (error) { return reject(error) }
// get `value` and `done` const { value, done } = res if (done) { // if `done` is true, meaning there isn't any yield left. Then we can resolve(value) return resolve(value) } else { // if `done` is false, meaning there are still some yield left.
// `value` may be a normal value or a Promise object return Promise.resolve(value).then(val => go('next', val), err => go('throw', err)) } }
go("next") }) }}
const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res))

用法

异步/等待版本代码:

async function asyncFn() {  const num1 = await fn(1)  console.log(num1) // 2  const num2 = await fn(num1)  console.log(num2) // 4  const num3 = await fn(num2)  console.log(num3) // 8  return num3}const asyncRes = asyncFn()console.log(asyncRes) // an Promise objectasyncRes.then(res => console.log(res)) // 8

generatorToAsync 版本代码:

function* gen() {  const num1 = yield fn(1)  console.log(num1) // 2  const num2 = yield fn(num1)  console.log(num2) // 4  const num3 = yield fn(num2)  console.log(num3) // 8  return num3}
const genToAsync = generatorToAsync(gen)const asyncRes = genToAsync()console.log(asyncRes) // an Promise objectasyncRes.then(res => console.log(res)) // 8

结论

在本文中,我们首先了解了 async/await 的基本用法,然后详细介绍了生成器函数的用法。async/await 本质上是生成器函数的语法糖。最后,我们使用生成器函数来实现 async/await。
希望今天的内容对你有用,谢谢你的阅读。
最后,如果你觉得有帮助的话,请点赞我,关注我,并将它分享给你身边做开发的朋友,也许能够帮助到他,与你一起学习进步。


学习更多技能

请点击下方公众号

浏览 11
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报