7 張圖,20 分鐘搞定 async-await 原理!別拖了!
前言
大家好,我是林三心,以最通俗的話,講最難的知識點是我寫文章的宗旨
之前我發過一篇手寫 Promise 原理,最通俗易懂的版本!!!,帶大家基本瞭解了Promise
內部的實現原理,而提到Promise
,就不得不提一個東西,那就是async/await
,async/await
是一個很重要的語法糖
,他的作用是用同步的方式,執行異步操作。那麼今天我就帶大家一起實現一下async/await
吧!!!
async/await 用法
其實你要實現一個東西之前,最好是先搞清楚這兩樣東西
-
這個東西有什麼用?
-
這個東西是怎麼用的?
有什麼用?
async/await
的用處就是:用同步方式,執行異步操作,怎麼說呢?舉個例子
比如我現在有一個需求:先請求完接口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
})
})
或者我現在又有一個需求:先請求完接口1
,再拿接口1
返回的數據,去當做接口2
的請求參數,那我們也可以這麼做
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秒後執行完
}
fn()
需求二:
async function fn () {
const res1 = await request(5)
const res2 = await request(res1)
console.log(res2) // 2秒後輸出 20
}
fn()
截屏 2021-09-11 下午 9.57.58.png
其實就類似於生活中的排隊
,咱們生活中排隊買東西,肯定是要上一個人買完,才輪到下一個人。而上面也一樣,在async
函數中,await
規定了異步操作只能一個一個排隊執行,從而達到用同步方式,執行異步操作的效果,這裏注意了:await 只能在 async 函數中使用,不然會報錯哦
剛剛上面的例子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秒後執行完 同時輸出
}
fn()
可以看出,如果await
後面接的不是Promise
的話,有可能其實是達不到排隊
的效果的
說完await
,咱們聊聊async
吧,async
是一個位於 function 之前的前綴,只有async函數
中,才能使用await
。那async
執行完是返回一個什麼東西呢?
async function fn () {}
console.log(fn) // [AsyncFunction: fn]
console.log(fn()) // Promise {<fulfilled>: undefined}
可以看出,async函數
執行完會自動返回一個狀態爲fulfilled
的 Promise,也就是成功狀態,但是值卻是 undefined,那要怎麼才能使值不是 undefined 呢?很簡單,函數有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方法
進行輸出
總結
總結一下async/await
的知識點
-
await 只能在 async 函數中使用,不然會報錯
-
async 函數返回的是一個 Promise 對象,有無值看有無 return 值
-
await 後面最好是接 Promise,雖然接其他值也能達到排隊效果
-
async/await 作用是用同步方式,執行異步操作
什麼是語法糖?
前面說了,async/await
是一種語法糖
,誒!好多同學就會問,啥是語法糖
呢?我個人理解就是,語法糖
就是一個東西,這個東西你就算不用他,你用其他手段也能達到這個東西同樣的效果,但是可能就沒有這個東西這麼方便了。
-
舉個生活中的例子吧:你走路也能走到北京,但是你坐飛機會更快到北京。
-
舉個代碼中的例子吧:ES6 的
class
也是語法糖,因爲其實用普通function
也能實現同樣效果
迴歸正題,async/await
是一種語法糖
,那就說明用其他方式其實也可以實現他的效果,我們今天就是講一講怎麼去實現async/await
,用到的是 ES6 裏的迭代函數——generator函數
generator 函數
基本用法
generator函數
跟普通函數在寫法上的區別就是,多了一個星號*
,並且只有在generator函數
中才能使用yield
,什麼是yield
呢,他相當於generator函數
執行的中途暫停點
,比如下方有 3 個暫停點。而怎麼才能暫停後繼續走呢?那就得使用到next方法
,next方法
執行後會返回一個對象,對象中有value 和 done
兩個屬性
-
value:暫停點後面接的值,也就是 yield 後面接的值
-
done:是否 generator 函數已走完,沒走完爲 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 函數有無返回值
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 }
截屏 2021-09-11 下午 9.46.17.png
yield 後面接函數
yield 後面接函數的話,到了對應暫停點 yield,會馬上執行此函數,並且該函數的執行返回值,會被當做此暫停點對象的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
前面說了,函數執行返回值會當做暫停點對象的 value 值,那麼下面例子就可以理解了,前兩個的 value 都是 pending 狀態的 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 }
截屏 2021-09-11 下午 10.51.38.png
其實我們想要的結果是,兩個 Promise 的結果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 }
})
})
截屏 2021-09-11 下午 10.38.37.png
next 函數傳參
generator 函數可以用next方法
來傳參,並且可以通過yield
來接收這個參數,注意兩點
-
第一次 next 傳參是沒用的,只有從第二次開始 next 傳參纔有用
-
next 傳值時,要記住順序是,先右邊 yield,後左邊接收參數
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 }
截屏 2021-09-11 下午 10.53.02.png
Promise+next 傳參
前面講了
-
yield 後面接 Promise
-
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) // 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 }
})
})
})
截屏 2021-09-11 下午 11.05.44.png
實現 async/await
其實上方的generator函數
的Promise+next傳參
,就很像async/await
了,區別在於
-
gen 函數執行返回值不是 Promise,asyncFn 執行返回值是 Promise
-
gen 函數需要執行相應的操作,才能等同於 asyncFn 的排隊效果
-
gen 函數執行的操作是不完善的,因爲並不確定有幾個 yield,不確定會嵌套幾次
截屏 2021-09-11 下午 11.53.41.png
那我們怎麼辦呢?我們可以封裝一個高階函數。什麼是高階函數
呢?高階函數
的特點是:參數是函數,返回值也可以是函數。下方的highorderFn
就是一個高階函數
function highorderFn(函數) {
// 一系列處理
return 函數
}
我們可以封裝一個高階函數,接收一個 generator 函數,並經過一系列處理,返回一個具有 async 函數功能的函數
function generatorToAsync(generatorFn) {
// 經過一系列處理
return 具有async函數功能的函數
}
返回值 Promise
之前我們說到,async 函數的執行返回值是一個 Promise,那我們要怎麼實現相同的結果呢
function* gen() {
}
const asyncFn = generatorToAsync(gen)
console.log(asyncFn()) // 期望這裏輸出 Promise
其實很簡單,generatorToAsync函數
裏做一下處理就行了
function* gen() {
}
function generatorToAsync (generatorFn) {
return function () {
return new Promise((resolve, reject) => {
})
}
}
const asyncFn = generatorToAsync(gen)
console.log(asyncFn()) // Promise
加入一系列操作
咱們把之前的處理代碼,加入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) // 傳入上次的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
可以發現,咱們其實已經實現了以下的async/await
的結果了
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 函數中可能有 2 個 await,3 個 await,5 個 await ,其實 await 的個數是不確定的。同樣類比,generator 函數中,也可能有 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) // 這裏有可能會執行返回reject狀態的Promise
} catch (error) {
return reject(error) // 報錯的話會走catch,直接reject
}
// 解構獲得value和done
const { value, done } = res
if (done) {
// 如果done爲true,說明走完了,進行resolve(value)
return resolve(value)
} else {
// 如果done爲false,說明沒走完,還得繼續走
// value有可能是:常量,Promise,Promise有可能是成功或者失敗
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))
這樣的話,無論是多少個 yield 都會排隊執行了,咱們把代碼寫成活的了
示例
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函數
的版本
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
結語
喜歡的朋友可以支持一下我,點擊一下點贊
或者在看
,也可以關注我的公衆號【前端之神】,加我好友,拉你進【千人學習大羣】哦!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/RFK0QWeE-jXRUJUR0KDpSA