7張圖,20分鐘就能搞定的async/await原理!
前言
大家好,我是林三心,以最通俗的話,講最難的知識點是我寫文章的宗旨
之前我發(fā)過一篇手寫Promise原理,最通俗易懂的版本?。?!,帶大家基本了解了Promise內(nèi)部的實現(xiàn)原理,而提到Promise,就不得不提一個東西,那就是async/await,async/await是一個很重要的語法糖,他的作用是用同步的方式,執(zhí)行異步操作。那么今天我就帶大家一起實現(xiàn)一下async/await吧?。?!
async/await用法
其實你要實現(xiàn)一個東西之前,最好是先搞清楚這兩樣?xùn)|西
這個東西有什么用? 這個東西是怎么用的?
有什么用?
async/await的用處就是:用同步方式,執(zhí)行異步操作,怎么說呢?舉個例子
比如我現(xiàn)在有一個需求:先請求完接口1,再去請求接口2,我們通常會這么做
function request(num) { // 模擬接口請求
return new Promise(resolve => {
setTimeout(() => {
resolve(num * 2)
}, 1000)
})
}
request(1).then(res1 => {
console.log(res1) // 1秒后 輸出 2
request(2).then(res2 => {
console.log(res2) // 2秒后 輸出 4
})
})
或者我現(xiàn)在又有一個需求:先請求完接口1,再拿接口1返回的數(shù)據(jù),去當(dāng)做接口2的請求參數(shù),那我們也可以這么做
request(5).then(res1 => {
console.log(res1) // 1秒后 輸出 10
request(res1).then(res2 => {
console.log(res2) // 2秒后 輸出 20
})
})
其實這么做是沒問題的,但是如果嵌套的多了,不免有點不雅觀,這個時候就可以用async/await來解決了
async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒后輸出 20
}
fn()
是怎么用?
還是用剛剛的例子
需求一:
async function fn () {
await request(1)
await request(2)
// 2秒后執(zhí)行完
}
fn()
需求二:
async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒后輸出 20
}
fn()

其實就類似于生活中的排隊,咱們生活中排隊買東西,肯定是要上一個人買完,才輪到下一個人。而上面也一樣,在async函數(shù)中,await規(guī)定了異步操作只能一個一個排隊執(zhí)行,從而達(dá)到用同步方式,執(zhí)行異步操作的效果,這里注意了:await只能在async函數(shù)中使用,不然會報錯哦
剛剛上面的例子await后面都是跟著異步操作Promise,那如果不接Promise會怎么樣呢?
function request(num) { // 去掉Promise
setTimeout(() => {
console.log(num * 2)
}, 1000)
}
async function fn() {
await request(1) // 2
await request(2) // 4
// 1秒后執(zhí)行完 同時輸出
}
fn()
可以看出,如果await后面接的不是Promise的話,其實是達(dá)不到排隊的效果的
說完await,咱們聊聊async吧,async是一個位于function之前的前綴,只有async函數(shù)中,才能使用await。那async執(zhí)行完是返回一個什么東西呢?
async function fn () {}
console.log(fn) // [AsyncFunction: fn]
console.log(fn()) // Promise {<fulfilled>: undefined}
可以看出,async函數(shù)執(zhí)行完會自動返回一個狀態(tài)為fulfilled的Promise,也就是成功狀態(tài),但是值卻是undefined,那要怎么才能使值不是undefined呢?很簡單,函數(shù)有return返回值就行了
async function fn (num) {
return num
}
console.log(fn) // [AsyncFunction: fn]
console.log(fn(10)) // Promise {<fulfilled>: 10}
fn(10).then(res => console.log(res)) // 10
可以看出,此時就有值了,并且還能使用then方法進(jìn)行輸出
總結(jié)
總結(jié)一下async/await的知識點
await只能在async函數(shù)中使用,不然會報錯 async函數(shù)返回的是一個狀態(tài)為fuifilled的Promise對象,有無值看有無return值 await后面只有接了Promise才能實現(xiàn)排隊效果 async/await作用是用同步方式,執(zhí)行異步操作
什么是語法糖?
前面說了,async/await是一種語法糖,誒!好多同學(xué)就會問,啥是語法糖呢?我個人理解就是,語法糖就是一個東西,這個東西你就算不用他,你用其他手段也能達(dá)到這個東西同樣的效果,但是可能就沒有這個東西這么方便了。
舉個生活中的例子吧:你走路也能走到北京,但是你坐飛機(jī)會更快到北京。 舉個代碼中的例子吧:ES6的 class也是語法糖,因為其實用普通function也能實現(xiàn)同樣效果
回歸正題,async/await是一種語法糖,那就說明用其他方式其實也可以實現(xiàn)他的效果,我們今天就是講一講怎么去實現(xiàn)async/await,用到的是ES6里的迭代函數(shù)——generator函數(shù)
generator函數(shù)
基本用法
generator函數(shù)跟普通函數(shù)在寫法上的區(qū)別就是,多了一個星號*,并且只有在generator函數(shù)中才能使用yield,什么是yield呢,他相當(dāng)于generator函數(shù)執(zhí)行的中途暫停點,比如下方有3個暫停點。而怎么才能暫停后繼續(xù)走呢?那就得使用到next方法,next方法執(zhí)行后會返回一個對象,對象中有value 和 done兩個屬性
value:暫停點后面接的值,也就是yield后面接的值 done:是否generator函數(shù)已走完,沒走完為false,走完為true
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 }
可以看到最后一個是undefined,這取決于你generator函數(shù)有無返回值
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后面接函數(shù)
yield后面接函數(shù)的話,到了對應(yīng)暫停點yield,會馬上執(zhí)行此函數(shù),并且該函數(shù)的執(zhí)行返回值,會被當(dāng)做此暫停點對象的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 }
yield后面接Promise
前面說了,函數(shù)執(zhí)行返回值會當(dāng)做暫停點對象的value值,那么下面例子就可以理解了,前兩個的value都是pending狀態(tài)的Promise對象
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 }

其實我們想要的結(jié)果是,兩個Promise的結(jié)果1 和 2,那怎么做呢?很簡單,使用Promise的then方法就行了
const g = gen()
const next1 = g.next()
next1.value.then(res1 => {
console.log(next1) // 1秒后輸出 { value: Promise { 1 }, done: false }
console.log(res1) // 1秒后輸出 1
const next2 = g.next()
next2.value.then(res2 => {
console.log(next2) // 2秒后輸出 { value: Promise { 2 }, done: false }
console.log(res2) // 2秒后輸出 2
console.log(g.next()) // 2秒后輸出 { value: 3, done: true }
})
})

next函數(shù)傳參
generator函數(shù)可以用next方法來傳參,并且可以通過yield來接收這個參數(shù),注意兩點
第一次next傳參是沒用的,只有從第二次開始next傳參才有用 next傳值時,要記住順序是,先右邊yield,后左邊接收參數(shù)
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 }

Promise+next傳參
前面講了
yield后面接Promise next函數(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) // 1秒后同時輸出 { value: Promise { 2 }, done: false }
console.log(res1) // 1秒后同時輸出 2
const next2 = g.next(res1) // 傳入上次的res1
next2.value.then(res2 => {
console.log(next2) // 2秒后同時輸出 { value: Promise { 4 }, done: false }
console.log(res2) // 2秒后同時輸出 4
const next3 = g.next(res2) // 傳入上次的res2
next3.value.then(res3 => {
console.log(next3) // 3秒后同時輸出 { value: Promise { 8 }, done: false }
console.log(res3) // 3秒后同時輸出 8
// 傳入上次的res3
console.log(g.next(res3)) // 3秒后同時輸出 { value: 8, done: true }
})
})
})

實現(xiàn)async/await
其實上方的generator函數(shù)的Promise+next傳參,就很像async/await了,區(qū)別在于
gen函數(shù)執(zhí)行返回值不是Promise,asyncFn執(zhí)行返回值是Promise gen函數(shù)需要執(zhí)行相應(yīng)的操作,才能等同于asyncFn的排隊效果 gen函數(shù)執(zhí)行的操作是不完善的,因為并不確定有幾個yield,不確定會嵌套幾次

那我們怎么辦呢?我們可以封裝一個高階函數(shù)。什么是高階函數(shù)呢?高階函數(shù)的特點是:參數(shù)是函數(shù),返回值也是函數(shù)。下方的highorderFn就是一個高階函數(shù)
function highorderFn(函數(shù)) {
// 一系列處理
return 函數(shù)
}
我們可以封裝一個高階函數(shù),接收一個generator函數(shù),并經(jīng)過一系列處理,返回一個具有async函數(shù)功能的函數(shù)
function generatorToAsync(generatorFn) {
// 經(jīng)過一系列處理
return 具有async函數(shù)功能的函數(shù)
}
返回值Promise
之前我們說到,async函數(shù)的執(zhí)行返回值是一個Promise,那我們要怎么實現(xiàn)相同的結(jié)果呢
function* gen() {
}
const asyncFn = generatorToAsync(gen)
console.log(asyncFn()) // 期望這里輸出 Promise
其實很簡單,generatorToAsync函數(shù)里做一下處理就行了
function* gen() {
}
function generatorToAsync (generatorFn) {
return function () {
return new Promise((resolve, reject) => {
})
}
}
const asyncFn = generatorToAsync(gen)
console.log(asyncFn()) // Promise
加入一系列操作
咱們把之前的處理代碼,加入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) // 傳入上次的res1
next2.value.then(res2 => {
const next3 = g.next(res2) // 傳入上次的res2
next3.value.then(res3 => {
// 傳入上次的res3
resolve(g.next(res3).value)
})
})
})
})
}
}
const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res)) // 3秒后輸出 8
可以發(fā)現(xiàn),咱們其實已經(jīng)實現(xiàn)了以下的async/await的結(jié)果了
async function asyncFn() {
const num1 = await fn(1)
const num2 = await fn(num1)
const num3 = await fn(num2)
return num3
}
asyncFn().then(res => console.log(res)) // 3秒后輸出 8
完善代碼
上面的代碼其實都是死代碼,因為一個async函數(shù)中可能有2個await,3個await,5個await ,其實await的個數(shù)是不確定的。同樣類比,generator函數(shù)中,也可能有2個yield,3個yield,5個yield,所以咱們得把代碼寫成活的才行
function generatorToAsync(generatorFn) {
return function() {
const gen = generatorFn.apply(this, arguments) // gen有可能傳參
// 返回一個Promise
return new Promise((resolve, reject) => {
function go(key, arg) {
let res
try {
res = gen[key](arg) // 這里有可能會執(zhí)行返回reject狀態(tài)的Promise
} catch (error) {
return reject(error) // 報錯的話會走catch,直接reject
}
// 解構(gòu)獲得value和done
const { value, done } = res
if (done) {
// 如果done為true,說明走完了,進(jìn)行resolve(value)
return resolve(value)
} else {
// 如果done為false,說明沒走完,還得繼續(xù)走
// value有可能是:常量,Promise,Promise有可能是成功或者失敗
return Promise.resolve(value).then(val => go('next', val), err => go('throw', err))
}
}
go("next") // 第一次執(zhí)行
})
}
}
const asyncFn = generatorToAsync(gen)
asyncFn().then(res => console.log(res))
這樣的話,無論是多少個yield都會排隊執(zhí)行了,咱們把代碼寫成活的了
示例
async/await版本
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) // Promise
asyncRes.then(res => console.log(res)) // 8
使用generatorToAsync函數(shù)的版本
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) // Promise
asyncRes.then(res => console.log(res)) // 8
結(jié)語
如果你覺得此文對你有一丁點幫助,點個贊,鼓勵一下林三心哈哈?;蛘呖梢约尤胛业拿~群 想進(jìn)學(xué)習(xí)群,摸魚群,請加我vxmeron857287645我會定時直播模擬面試,答疑解惑
