用图表学习掌握 异步/同步知识
英文 | 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 fun
ction 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 object
asyncRes.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 object
asyncRes.then(res => console.log(res)) // 8
结论
学习更多技能
请点击下方公众号