
英文 | 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
結論
在本文中,我們首先了解了 async/await 的基本用法,然後詳細介紹了生成器函數的用法。async/await 本質上是生成器函數的文法糖。最後,我們使用生成器函數來實作 async/await。
希望今天的内容對你有用,謝謝你的閱讀。
最後,如果你覺得有幫助的話,請點贊我,關注我,并将它分享給你身邊做開發的朋友,也許能夠幫助到他,與你一起學習進步。