用圖表學(xué)習(xí)掌握 異步/同步知識

英文 | https://medium.com/frontend-canteen/you-can-master-async-await-with-7-diagrams-ac96a97abe92
翻譯 | 楊小愛
您可能已經(jīng)閱讀了一些關(guān)于 異步/同步 的文章,甚至使用它們編寫了一些代碼。但是你真的掌握了異步/同步嗎?
在本文中,讓我們討論以下主題:
異步/同步的基本用法。
然后我們了解異步的祖先,生成器函數(shù)。
最后,讓我們自己實(shí)現(xiàn) 異步/同步。
我準(zhǔn)備了 7 個圖表來解釋這些概念,希望它們能幫助您更輕松地理解這些主題。
異步/同步的基礎(chǔ)
一句話總結(jié)異步/同步的用法就是:以同步的方式進(jìn)行異步操作。
比如有這樣一個場景:我們需要請求一個API,收到響應(yīng)后,再請求另一個API。
然后我們可以這樣寫代碼:
function request(num) { // mock HTTP requestreturn new Promise(resolve => {setTimeout(() => {resolve(num * 2)}, 1000)})}request(1).then(res1 => {console.log(res1) // it will print `2` after 1 secondrequest(2).then(res2 => {console.log(res2) // it will print `4` after anther 1 second})})
或者還有另外一種場景:我們需要請求一個API,收到響應(yīng)后,再以之前的響應(yīng)作為參數(shù)請求另一個API。
然后我們可以這樣寫代碼:
request(5).then(res1 => {console.log(res1) // it will print `10` after 1 secondrequest(res1).then(res2 => {console.log(res2) // it will print `20` after anther 1 second})})
上面兩段代碼確實(shí)可以解決問題,但是如果嵌套層級太多,代碼就會不美觀、不可讀。
解決這個問題的方法是使用異步/同步。它允許我們以同步的方式執(zhí)行異步操作。
用異步/同步重構(gòu)以上兩段代碼后,它們看起來像這樣:
示例 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 關(guān)鍵字之后的表達(dá)式的結(jié)果返回,然后再繼續(xù)執(zhí)行下面的代碼。
以上代碼執(zhí)行流程示意圖:

就像你在加油站加油一樣,只有當(dāng)前一輛車加滿油后,才能輪到下一輛車加油。在async函數(shù)中,await指定異步操作只能在隊列中一個一個執(zhí)行,從而達(dá)到以同步方式執(zhí)行異步操作的效果。
注意:await 關(guān)鍵字只能用在 async 函數(shù)中,否則會報錯。
那我們要知道await后面不能跟普通函數(shù),否則就達(dá)不到排隊的效果。
下面的代碼是一個不正確的例子。
function request(num) {setTimeout(() => {console.log(num * 2)}, 1000)}async function fn() {await request(1) // 2await request(2) // 4// print `2` and `4` at the same time}fn()
生成器函數(shù)
async/await 本身的用法很簡單,但它實(shí)際上是一種語法糖。async/await 是 ES2017 中引入的一種語法。如果你嘗試將async/await語法的代碼編譯到ES2015版本,你會發(fā)現(xiàn)它們會被編譯成generate函數(shù),所以這里我們先了解generate函數(shù)。
生成器函數(shù)是使用 function* 語法編寫的。調(diào)用時,生成器函數(shù)最初不會執(zhí)行它們的代碼。相反,它們返回一種特殊類型的迭代器,稱為生成器。當(dāng)調(diào)用生成器的 next 方法消耗了一個值時,生成器函數(shù)會一直執(zhí)行,直到遇到 yield 關(guān)鍵字。
這是一個例子:
function* gen() {yield 1yield 2yield 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函數(shù)沒有返回值,所以最后一次調(diào)用g.next()返回的結(jié)果的value屬性是未定義的。
如果 generate 函數(shù)有返回值,那么最后一次調(diào)用 g.next() 返回的結(jié)果的 value 屬性就是結(jié)果。
function* gen() {yield 1yield 2yield 3return 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 }
如果我們用一張圖來表示上述函數(shù)的執(zhí)行,它應(yīng)該是這樣的:

yield a function
如果yield后面跟著函數(shù)調(diào)用,那么這里程序執(zhí)行完之后,會立即調(diào)用函數(shù)。并且函數(shù)的返回值會放在 g.next() 的結(jié)果的 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 之后。那么程序的執(zhí)行流程和之前一樣。
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 }
此代碼的執(zhí)行流程示意圖:

但是,我們要的不是處于pending狀態(tài)的Promise對象,而是Promise完成后存儲在其中的值。那么我們?nèi)绾涡薷纳厦娴拇a呢?
很簡單,我們只需要調(diào)用 .then 方法:
const g = gen()const next1 = g.next()next1.value.then(res1 => {console.log(next1) // print { value: Promise { 1 }, done: false } after 1 secondconsole.log(res1) // print `1` after 1 secondconst next2 = g.next()next2.value.then(res2 => {console.log(next2) // print { value: Promise { 2 }, done: false } after 2 secondsconsole.log(res2) // print `2` after 2 secondsconsole.log(g.next()) // print { value: 3, done: true } after 2 seconds})})
以上代碼執(zhí)行流程示意圖:

在 next() 中傳遞一個參數(shù)
然后,在調(diào)用 next() 函數(shù)時,我們可以傳遞參數(shù)。
function* gen() {const num1 = yield 1console.log(num1)const num2 = yield 2console.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 }
這里需要注意的是,第一次調(diào)用next()方法時,傳參是沒有作用的。
每次調(diào)用 g.next() 時,返回的結(jié)果都與我們之前的情況沒有什么不同。而num1會接受g.next(11111)的參數(shù)11111,num2會接受g.next(11111)的參數(shù)22222。
此代碼的執(zhí)行流程示意圖:

Promise + Pass param
之前我們提到過Promise對象可以放在yield之后,我們也提到過可以在next函數(shù)中傳入?yún)?shù)。
如果我們將這兩個功能放在一起,它會變成這樣:
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 secondconsole.log(res1) // print `2` after 1 senondconst next2 = g.next(res1) // pass privouse resultnext2.value.then(res2 => {console.log(next2) // print { value: Promise { 4 }, done: false } after 2 secondsconsole.log(res2) // print `4` after 2 senondconst next3 = g.next(res2) // pass privouse result `res2`next3.value.then(res3 => {console.log(next3) // print { value: Promise { 8 }, done: false } after 3 secondsconsole.log(res3) // print `8` after 3 senond// pass privouse result `res3`console.log(g.next(res3)) // print { value: 8, done: true } after 3 seconds})})})

其實(shí)上面的寫法和async/await很像。
唯一的區(qū)別是:
gen函數(shù)執(zhí)行后,返回值不是Promise對象。但是 asyncFn 的返回值是 Promise
gen函數(shù)需要執(zhí)行特定的操作才相當(dāng)于asyncFn的排隊效果
gen函數(shù)執(zhí)行的操作是不完善的,它規(guī)定只能處理三層嵌套
下面我們將解決這些問題并自己實(shí)現(xiàn) async/await。
實(shí)現(xiàn)async/await

為了解決前面提到的問題,我們可以封裝一個高階函數(shù)。這個高階函數(shù)可以接受一個生成器函數(shù),經(jīng)過一系列的處理,返回一個新的函數(shù),工作起來就像一個真正的異步函數(shù)。
function generatorToAsync(generatorFn) {// do somethingreturn `a function works like a real async function`}
異步函數(shù)的返回值應(yīng)該是一個 Promise 對象,所以我們的 generatorToAsync 函數(shù)的模板應(yīng)該是這樣的:
function* gen() {}function generatorToAsync (generatorFn) {return function () {return new Promise((resolve, reject) => {})}}const asyncFn = generatorToAsync(gen)console.log(asyncFn()) // an Promise object
然后,我們可以將前面的代碼復(fù)制到 generatorToAsync 函數(shù)中:
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,而在實(shí)際項目中,yield的個數(shù)是不確定的,可能是3、5或10。所以我們還需要調(diào)整代碼,讓我們的generatorToAsync函數(shù)可以處理任何 產(chǎn)量數(shù):
function generatorToAsync(generatorFn) {return function() {const gen = generatorFn.apply(this, arguments) // there may be arguments of gen function// return a Promise objectreturn new Promise((resolve, reject) => {function go(key, arg) {let restry {res = gen[key](arg)} catch (error) {return reject(error)}// get `value` and `done`const { value, done } = resif (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 objectreturn 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) // 2const num2 = await fn(num1)console.log(num2) // 4const num3 = await fn(num2)console.log(num3) // 8return 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) // 2const num2 = yield fn(num1)console.log(num2) // 4const num3 = yield fn(num2)console.log(num3) // 8return num3}const genToAsync = generatorToAsync(gen)const asyncRes = genToAsync()console.log(asyncRes) // an Promise objectasyncRes.then(res => console.log(res)) // 8
結(jié)論
學(xué)習(xí)更多技能
請點(diǎn)擊下方公眾號
![]()

