筆者剛接觸
async/await
時,就被其暫停執行的特性吸引了,心想在沒有原生API支援的情況下,await居然能挂起目前方法,實作暫停執行,我感到十分好奇。好奇心驅使我一層一層剝開有關JS異步程式設計的一切。閱讀完本文,讀者應該能夠了解:
-
的實作原理Promise
-
的實作原理async/await
-
的實作原理Generator
Promise實作
在成文過程中,筆者查閱了很多講解Promise實作的文章,但感覺大多文章都很難稱得上條理清晰,有的上來就放大段Promise規範翻譯,有的在Promise基礎使用上浪費篇幅,又或者把一個簡單的東西長篇大論,過度講解,我推薦頭鐵的同學直接拉到本章小結看最終實作,結合着注釋直接啃代碼也能了解十之八九
回歸正題,文章開頭我們先點一下Promise為我們解決了什麼問題:在傳統的異步程式設計中,如果異步之間存在依賴關系,我們就需要通過層層嵌套回調來滿足這種依賴,如果嵌套層數過多,可讀性和可維護性都變得很差,産生所謂“回調地獄”,而Promise将回調嵌套改為鍊式調用,增加可讀性和可維護性。下面我們就來一步步實作一個Promise:
1. 觀察者模式
我們先來看一個最簡單的Promise使用:
const p1 = new Promise((resolve, reject) => {
setTimeout(() {
resolve('result')
},
1000);
})
p1.then(res console.log(res), err => console.log(err))
觀察這個例子,我們分析Promise的調用流程:
-
的構造方法接收一個Promise
,在executor()
時就立刻執行這個executor回調new Promise()
-
内部的異步任務被放入宏/微任務隊列,等待執行executor()
-
被執行,收內建功/失敗回調,放入成功/失敗隊列then()
-
的異步任務被執行,觸發executor()
,從成功/失敗隊列中取出回調依次執行resolve/reject
其實熟悉設計模式的同學,很容易就能意識到這是個「觀察者模式」,這種
收集依賴 -> 觸發通知 -> 取出依賴執行
的方式,被廣泛運用于觀察者模式的實作,在Promise裡,執行順序是
then收集依賴 -> 異步觸發resolve -> resolve執行依賴
。依此,我們可以勾勒出Promise的大緻形狀:
class MyPromise {
// 構造方法接收一個回調
constructor(executor) {
this._resolveQueue = [] // then收集的執行成功的回調隊列
this._rejectQueue = [] // then收集的執行失敗的回調隊列
// 由于resolve/reject是在executor内部被調用, 是以需要使用箭頭函數固定this指向, 否則找不到this._resolveQueue
let _resolve = (val) => {
// 從成功隊列裡取出回調依次執行
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 實作同resolve
let _reject = (val) => {
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise()時立即執行executor,并傳入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一個成功的回調和一個失敗的回調,并push進對應隊列
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}
寫完代碼我們可以測試一下:
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() {
resolve('result')
}, 1000);
})
p1.then(res console.log(res))
//一秒後輸出result
我們運用觀察者模式簡單的實作了一下
then
和
resolve
,使我們能夠在then方法的回調裡取得異步操作的傳回值,但我們這個Promise離最終實作還有很長的距離,下面我們來一步步補充這個Promise:
2. Promise A+規範
上面我們已經簡單地實作了一個超低配版Promise,但我們會看到很多文章和我們寫的不一樣,他們的Promise實作中還引入了各種狀态控制,這是由于ES6的Promise實作需要遵循Promise/A+規範,是規範對Promise的狀态控制做了要求。Promise/A+的規範比較長,這裡隻總結兩條核心規則:
❝❞
- Promise本質是一個狀态機,且狀态隻能為以下三種:
、
Pending(等待态)
、
Fulfilled(執行态)
,狀态的變更是單向的,隻能從Pending -> Fulfilled 或 Pending -> Rejected,狀态變更不可逆
Rejected(拒絕态)
-
接收兩個可選參數,分别對應狀态改變時觸發的回調。then方法傳回一個promise。then 方法可以被同一個 promise 調用多次。
then方法
根據規範,我們補充一下Promise的代碼:
//Promise/A+規範的三種狀态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 構造方法接收一個回調
constructor(executor) {
this._status = PENDING // Promise狀态
this._resolveQueue = [] // 成功隊列, resolve時觸發
this._rejectQueue = [] // 失敗隊列, reject時觸發
// 由于resolve/reject是在executor内部被調用, 是以需要使用箭頭函數固定this指向, 否則找不到this._resolveQueue
let _resolve = (val) => {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = FULFILLED // 變更狀态
// 這裡之是以使用一個隊列來儲存回調,是為了實作規範要求的 "then 方法可以被同一個 promise 調用多次"
// 如果使用一個變量而非隊列來儲存回調,那麼即使多次p1.then()也隻會執行一次回調
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 實作同resolve
let _reject = (val) => {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = REJECTED // 變更狀态
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise()時立即執行executor,并傳入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一個成功的回調和一個失敗的回調
then(resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}
3. then的鍊式調用
補充完規範,我們接着來實作鍊式調用,這是Promise實作的重點和難點,我們先來看一下then是如何鍊式調用的:
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
p1
.then(res {
console.log(res)
//then回調中可以return一個Promise
return new Promise((resolve, reject) => {
setTimeout(() {
resolve(2)
}, 1000);
})
})
.then(res {
console.log(res)
//then回調中也可以return一個值
return 3
})
.then(res {
console.log(res)
})
輸出
1
2
3
我們思考一下如何實作這種鍊式調用:
- 顯然
需要傳回一個Promise,這樣才能找到then方法,是以我們會把then方法的傳回值包裝成Promise。.then()
-
的回調需要順序執行,以上面這段代碼為例,雖然中間return了一個Promise,但執行順序仍要保證是1->2->3。我們要等待目前Promise狀态變更後,再執行下一個then收集的回調,這就要求我們對then的傳回值分類讨論.then()
// then方法
then(resolveFn, rejectFn) {
//return一個新的promise
return new Promise((resolve, reject) => {
//把resolveFn重新包裝一下,再push進resolve執行隊列,這是為了能夠擷取回調的傳回值進行分類讨論
const fulfilledFn = value {
try {
//執行第一個(目前的)Promise的成功回調,并擷取傳回值
let x = resolveFn(value)
//分類讨論傳回值,如果是Promise,那麼等待Promise狀态變更,否則直接resolve
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
//把後續then收集的依賴都push進目前Promise的成功回調隊列中(_rejectQueue), 這是為了保證順序調用
this._resolveQueue.push(fulfilledFn)
//reject同理
const rejectedFn = error {
try {
let x = rejectFn(error)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
this._rejectQueue.push(rejectedFn)
})
}
然後我們就能測試一下鍊式調用:
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() {
resolve(1)
}, 500);
})
p1
.then(res {
console.log(res)
return 2
})
.then(res {
console.log(res)
return 3
})
.then(res {
console.log(res)
})
//輸出 1 2 3
4.值穿透 & 狀态已變更的情況
我們已經初步完成了鍊式調用,但是對于 then() 方法,我們還要兩個細節需要處理一下
- 「值穿透」:根據規範,如果 then() 接收的參數不是function,那麼我們應該忽略它。如果沒有忽略,當then()回調不為function時将會抛出異常,導緻鍊式調用中斷
- 「處理狀态為resolve/reject的情況」:其實我們上邊 then() 的寫法是對應狀态為
的情況,但是有些時候,resolve/reject 在 then() 之前就被執行(比如padding
),如果這個時候還把then()回調push進resolve/reject的執行隊列裡,那麼回調将不會被執行,是以對于狀态已經變為Promise.resolve().then()
或fulfilled
的情況,我們直接執行then回調:rejected
// then方法,接收一個成功的回調和一個失敗的回調
then(resolveFn, rejectFn) {
// 根據規範,如果then的參數不是function,則我們需要忽略它, 讓鍊式調用繼續往下執行
typeof resolveFn !== 'function' ? resolveFn = value value : null
typeof rejectFn !== 'function' ? rejectFn = error error : null
// return一個新的promise
return new Promise((resolve, reject) => {
// 把resolveFn重新包裝一下,再push進resolve執行隊列,這是為了能夠擷取回調的傳回值進行分類讨論
const fulfilledFn = value {
try {
// 執行第一個(目前的)Promise的成功回調,并擷取傳回值
let x = resolveFn(value)
// 分類讨論傳回值,如果是Promise,那麼等待Promise狀态變更,否則直接resolve
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error {
try {
let x = rejectFn(error)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
// 當狀态為pending時,把then回調push進resolve/reject執行隊列,等待執行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 當狀态已經變為resolve/reject時,直接執行then回調
case FULFILLED:
fulfilledFn(this._value) // this._value是上一個then回調return的值(見完整版代碼)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
5.相容同步任務
完成了then的鍊式調用以後,我們再處理一個前邊的細節,然後放出完整代碼。上文我們說過,Promise的執行順序是
new Promise -> then()收集回調 -> resolve/reject執行回調
,這一順序是建立在「executor是異步任務」的前提上的,如果executor是一個同步任務,那麼順序就會變成
new Promise -> resolve/reject執行回調 -> then()收集回調
,resolve的執行跑到then之前去了,為了相容這種情況,我們給
resolve/reject
執行回調的操作包一個setTimeout,讓它異步執行。
❝
這裡插一句,有關這個setTimeout,其實還有一番學問。雖然規範沒有要求回調應該被放進宏任務隊列還是微任務隊列,但其實Promise的預設實作是放進了微任務隊列,我們的實作(包括大多數Promise手動實作和polyfill的轉化)都是使用setTimeout放入了宏任務隊列(當然我們也可以用MutationObserver模拟微任務)
❞
//Promise/A+規定的三種狀态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 構造方法接收一個回調
constructor(executor) {
this._status = PENDING // Promise狀态
this._value = undefined // 儲存then回調return的值
this._resolveQueue = [] // 成功隊列, resolve時觸發
this._rejectQueue = [] // 失敗隊列, reject時觸發
// 由于resolve/reject是在executor内部被調用, 是以需要使用箭頭函數固定this指向, 否則找不到this._resolveQueue
let _resolve = (val) => {
//把resolve執行回調的操作封裝成一個函數,放進setTimeout裡,以相容executor是同步代碼的情況
const run = () {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = FULFILLED // 變更狀态
this._value = val // 儲存目前value
// 這裡之是以使用一個隊列來儲存回調,是為了實作規範要求的 "then 方法可以被同一個 promise 調用多次"
// 如果使用一個變量而非隊列來儲存回調,那麼即使多次p1.then()也隻會執行一次回調
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// 實作同resolve
let _reject = (val) => {
const run = () {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = REJECTED // 變更狀态
this._value = val // 儲存目前value
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// new Promise()時立即執行executor,并傳入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一個成功的回調和一個失敗的回調
then(resolveFn, rejectFn) {
// 根據規範,如果then的參數不是function,則我們需要忽略它, 讓鍊式調用繼續往下執行
typeof resolveFn !== 'function' ? resolveFn = value value : null
typeof rejectFn !== 'function' ? rejectFn = error error : null
// return一個新的promise
return new Promise((resolve, reject) => {
// 把resolveFn重新包裝一下,再push進resolve執行隊列,這是為了能夠擷取回調的傳回值進行分類讨論
const fulfilledFn = value {
try {
// 執行第一個(目前的)Promise的成功回調,并擷取傳回值
let x = resolveFn(value)
// 分類讨論傳回值,如果是Promise,那麼等待Promise狀态變更,否則直接resolve
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error {
try {
let x = rejectFn(error)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
// 當狀态為pending時,把then回調push進resolve/reject執行隊列,等待執行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 當狀态已經變為resolve/reject時,直接執行then回調
case FULFILLED:
fulfilledFn(this._value) // this._value是上一個then回調return的值(見完整版代碼)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
}
然後我們可以測試一下這個Promise:
const p1 = new MyPromise((resolve, reject) => {
resolve(1) //同步executor測試
})
p1
.then(res {
console.log(res)
return 2 //鍊式調用測試
})
.then() //值穿透測試
.then(res {
console.log(res)
return new MyPromise((resolve, reject) => {
resolve(3) //傳回Promise測試
})
})
.then(res {
console.log(res)
throw new Error('reject測試') //reject測試
})
.then(() {}, err => {
console.log(err)
})
// 輸出
// 1
// 2
// 3
// Error: reject測試
到這裡,我們已經實作了Promise的主要功能
(`∀´)Ψ
剩下的幾個方法都非常簡單,我們順手收拾掉:
Promise.prototype.catch()
❝
catch()方法
傳回一個Promise,并且處理拒絕的情況。它的行為與調用Promise.prototype.then(undefined, onRejected) 相同。
❞
//catch方法其實就是執行一下then的第二個回調
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
Promise.prototype.finally()
❝
finally()方法
傳回一個Promise。在promise結束時,無論結果是fulfilled或者是rejected,都會執行指定的回調函數。在finally之後,還可以繼續then。并且會将值原封不動的傳遞給後面的then
❞
//finally方法
finally(callback) {
return this.then(
value MyPromise.resolve(callback()).then(() value), // MyPromise.resolve執行回調,并在then中return結果傳遞給後面的Promise
reason => MyPromise.resolve(callback()).then(() { throw reason }) // reject同理
)
}
Promise.resolve()
❝
Promise.resolve(value)
方法傳回一個以給定值解析後的Promise 對象。如果該值為promise,傳回這個promise;如果這個值是thenable(即帶有"then" 方法)),傳回的promise會“跟随”這個thenable的對象,采用它的最終狀态;否則傳回的promise将以此值完成。此函數将類promise對象的多層嵌套展平。
❞
//靜态的resolve方法
static resolve(value) {
if(value instanceof MyPromise) return value // 根據規範, 如果參數是Promise執行個體, 直接return這個執行個體
return new MyPromise(resolve resolve(value))
}
Promise.reject()
❝
Promise.reject()
方法傳回一個帶有拒絕原因的Promise對象。
❞
//靜态的reject方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}
Promise.all()
❝
Promise.all(iterable)
方法傳回一個 Promise 執行個體,此執行個體在 iterable 參數内所有的 promise 都“完成(resolved)”或參數中不包含 promise 時回調完成(resolve);如果參數中 promise 有一個失敗(rejected),此執行個體回調失敗(reject),失敗原因的是第一個失敗 promise 的結果。
❞
//靜态的all方法
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于處理傳入值不為Promise的情況
MyPromise.resolve(p).then(
val {
index++
result[i] = val
//所有then執行後, resolve結果
if(index === promiseArr.length) {
resolve(result)
}
},
err => {
//有一個Promise被reject時,MyPromise的狀态變為reject
reject(err)
}
)
})
})
}
Promise.race()
❝
Promise.race(iterable)
方法傳回一個 promise,一旦疊代器中的某個promise解決或拒絕,傳回的 promise就會解決或拒絕。
❞
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同時執行Promise,如果有一個Promise的狀态發生改變,就變更新MyPromise的狀态
for (let p of promiseArr) {
Promise.resolve(p).then( //Promise.resolve(p)用于處理傳入值不為Promise的情況
value {
resolve(value) //注意這個resolve是上邊new MyPromise的
},
err => {
reject(err)
}
)
}
})
}
完整代碼
//Promise/A+規定的三種狀态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
// 構造方法接收一個回調
constructor(executor) {
this._status = PENDING // Promise狀态
this._value = undefined // 儲存then回調return的值
this._resolveQueue = [] // 成功隊列, resolve時觸發
this._rejectQueue = [] // 失敗隊列, reject時觸發
// 由于resolve/reject是在executor内部被調用, 是以需要使用箭頭函數固定this指向, 否則找不到this._resolveQueue
let _resolve = (val) => {
//把resolve執行回調的操作封裝成一個函數,放進setTimeout裡,以相容executor是同步代碼的情況
const run = () {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = FULFILLED // 變更狀态
this._value = val // 儲存目前value
// 這裡之是以使用一個隊列來儲存回調,是為了實作規範要求的 "then 方法可以被同一個 promise 調用多次"
// 如果使用一個變量而非隊列來儲存回調,那麼即使多次p1.then()也隻會執行一次回調
while(this._resolveQueue.length) {
const callback = this._resolveQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// 實作同resolve
let _reject = (val) => {
const run = () {
if(this._status !== PENDING) return // 對應規範中的"狀态隻能由pending到fulfilled或rejected"
this._status = REJECTED // 變更狀态
this._value = val // 儲存目前value
while(this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
setTimeout(run)
}
// new Promise()時立即執行executor,并傳入resolve和reject
executor(_resolve, _reject)
}
// then方法,接收一個成功的回調和一個失敗的回調
then(resolveFn, rejectFn) {
// 根據規範,如果then的參數不是function,則我們需要忽略它, 讓鍊式調用繼續往下執行
typeof resolveFn !== 'function' ? resolveFn = value value : null
typeof rejectFn !== 'function' ? rejectFn = error error : null
// return一個新的promise
return new Promise((resolve, reject) => {
// 把resolveFn重新包裝一下,再push進resolve執行隊列,這是為了能夠擷取回調的傳回值進行分類讨論
const fulfilledFn = value {
try {
// 執行第一個(目前的)Promise的成功回調,并擷取傳回值
let x = resolveFn(value)
// 分類讨論傳回值,如果是Promise,那麼等待Promise狀态變更,否則直接resolve
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error {
try {
let x = rejectFn(error)
x instanceof Promise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
switch (this._status) {
// 當狀态為pending時,把then回調push進resolve/reject執行隊列,等待執行
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break;
// 當狀态已經變為resolve/reject時,直接執行then回調
case FULFILLED:
fulfilledFn(this._value) // this._value是上一個then回調return的值(見完整版代碼)
break;
case REJECTED:
rejectedFn(this._value)
break;
}
})
}
//catch方法其實就是執行一下then的第二個回調
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
//finally方法
finally(callback) {
return this.then(
value MyPromise.resolve(callback()).then(() value), //執行回調,并returnvalue傳遞給後面的then
reason => MyPromise.resolve(callback()).then(() { throw reason }) //reject同理
)
}
//靜态的resolve方法
static resolve(value) {
if(value instanceof MyPromise) return value //根據規範, 如果參數是Promise執行個體, 直接return這個執行個體
return new MyPromise(resolve resolve(value))
}
//靜态的reject方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}
//靜态的all方法
static all(promiseArr) {
let index = 0
let result = []
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p, i) => {
//Promise.resolve(p)用于處理傳入值不為Promise的情況
MyPromise.resolve(p).then(
val {
index++
result[i] = val
if(index === promiseArr.length) {
resolve(result)
}
},
err => {
reject(err)
}
)
})
})
}
//靜态的race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同時執行Promise,如果有一個Promise的狀态發生改變,就變更新MyPromise的狀态
for (let p of promiseArr) {
Promise.resolve(p).then( //Promise.resolve(p)用于處理傳入值不為Promise的情況
value {
resolve(value) //注意這個resolve是上邊new MyPromise的
},
err => {
reject(err)
}
)
}
})
}
}
洋洋灑灑150多行的代碼,到這裡,我們終于可以給Promise的實作做一個結尾了。我們從一個最簡單的Promise使用執行個體開始,通過對調用流程的分析,根據觀察者模式實作了Promise的大緻骨架,然後依據Promise/A+規範填充代碼,重點實作了then 的鍊式調用,最後完成了Promise的靜态/執行個體方法。其實Promise實作在整體上并沒有太複雜的思想,但我們日常使用的時候往往忽略了很多Promise細節,因而很難寫出一個符合規範的Promise實作,源碼的實作過程,其實也是對Promise使用細節重新學習的過程。
async/await實作
雖然前邊花了這麼多篇幅講Promise的實作,不過探索
async/await
暫停執行的機制才是我們的初衷,下面我們就來進入這一塊的内容。同樣地,開頭我們點一下async/await的使用意義。在多個回調依賴的場景中,盡管Promise通過鍊式調用取代了回調嵌套,但過多的鍊式調用可讀性仍然不佳,流程控制也不友善,ES7 提出的async 函數,終于讓 JS 對于異步操作有了終極解決方案,簡潔優美地解決了以上兩個問題。
設想一個這樣的場景,異步任務a->b->c之間存在依賴關系,如果我們通過then鍊式調用來處理這些關系,可讀性并不是很好,如果我們想控制其中某個過程,比如在某些條件下,b不往下執行到c,那麼也不是很友善控制
Promise.resolve(a)
.then(b {
// do something
})
.then(c {
// do something
})
但是如果通過async/await來實作這個場景,可讀性和流程控制都會友善不少。
async () => {
const a = await Promise.resolve(a);
const b = await Promise.resolve(b);
const c = await Promise.resolve(c);
}
那麼我們要如何實作一個async/await呢,首先我們要知道,「async/await實際上是對Generator(生成器)的封裝」,是一個文法糖。由于Generator出現不久就被async/await取代了,很多同學對Generator比較陌生,是以我們先來看看Generator的用法:
❝
ES6 新引入了 Generator 函數,可以通過 yield 關鍵字,把函數的執行流挂起,通過next()方法可以切換到下一個狀态,為改變執行流程提供了可能,進而為異步程式設計提供解決方案。
❞
function* myGenerator() {
yield '1'
yield '2'
return '3'
}
const gen = myGenerator(); // 擷取疊代器
gen.next() //{value: "1", done: false}
gen.next() //{value: "2", done: false}
gen.next() //{value: "3", done: true}
也可以通過給
next()
傳參, 讓yield具有傳回值
function* myGenerator() {
console.log(yield '1') //test1
console.log(yield '2') //test2
console.log(yield '3') //test3
}
// 擷取疊代器
const gen = myGenerator();
gen.next()
gen.next('test1')
gen.next('test2')
gen.next('test3')
我們看到Generator的用法,應該️會感到很熟悉,
*/yield
和
async/await
看起來其實已經很相似了,它們都提供了暫停執行的功能,但二者又有三點不同:
-
自帶執行器,不需要手動調用next()就能自動執行下一步async/await
-
函數傳回值是Promise對象,而Generator傳回的是生成器對象async
-
能夠傳回Promise的resolve/reject的值await
我們對async/await的實作,其實也就是對應以上三點封裝Generator
1.自動執行
我們先來看一下,對于這樣一個Generator,手動執行是怎樣一個流程
function* myGenerator() {
yield Promise.resolve(1);
yield Promise.resolve(2);
yield Promise.resolve(3);
}
const gen = myGenerator()
gen.next().value.then(val {
console.log(val)
gen.next().value.then(val {
console.log(val)
gen.next().value.then(val {
console.log(val)
})
})
})
//輸出1 2 3
我們也可以通過給
gen.next()
傳值的方式,讓yield能傳回resolve的值
function* myGenerator() {
console.log(yield Promise.resolve(1)) //1
console.log(yield Promise.resolve(2)) //2
console.log(yield Promise.resolve(3)) //3
}
const gen = myGenerator()
gen.next().value.then(val {
// console.log(val)
gen.next(val).value.then(val {
// console.log(val)
gen.next(val).value.then(val {
// console.log(val)
gen.next(val)
})
})
})
顯然,手動執行的寫法看起來既笨拙又醜陋,我們希望生成器函數能自動往下執行,且yield能傳回resolve的值,基于這兩個需求,我們進行一個基本的封裝,這裡
async/await
是關鍵字,不能重寫,我們用函數來模拟:
function run(gen) {
var g = gen() //由于每次gen()擷取到的都是最新的疊代器,是以擷取疊代器操作要放在step()之前,否則會進入死循環
function step(val) { //封裝一個方法, 遞歸執行next()
var res = g.next(val) //擷取疊代器對象,并傳回resolve的值
if(res.done) return res.value //遞歸終止條件
res.value.then(val { //Promise的then方法是實作自動疊代的前提
step(val) //等待Promise完成就自動執行下一個next,并傳入resolve的值
})
}
step() //第一次執行
}
對于我們之前的例子,我們就能這樣執行:
function* myGenerator() {
console.log(yield Promise.resolve(1)) //1
console.log(yield Promise.resolve(2)) //2
console.log(yield Promise.resolve(3)) //3
}
run(myGenerator)
這樣我們就初步實作了一個
async/await
上邊的代碼隻有五六行,但并不是一下就能看明白的,我們之前用了四個例子來做鋪墊,也是為了讓讀者更好地了解這段代碼。簡單的說,我們封裝了一個run方法,run方法裡我們把執行下一步的操作封裝成step(),每次Promise.then()的時候都去執行step(),實作自動疊代的效果。在疊代的過程中,我們還把resolve的值傳入
gen.next()
,使得yield得以傳回Promise的resolve的值
❝
這裡插一句,是不是隻有
這樣的形式才能完成我們自動執行的功能呢?答案是否定的,yield後邊除了接Promise,還可以接
.then方法
thunk函數
,thunk函數不是一個新東西,所謂thunk函數,就是「單參的隻接受回調的函數」,詳細介紹可以看阮一峰Thunk 函數的含義和用法,無論是Promise還是thunk函數,其核心都是通過「傳入回調」的方式來實作Generator的自動執行。thunk函數隻作為一個拓展知識,了解有困難的同學也可以跳過這裡,并不影響後續了解。
❞
2.傳回Promise & 異常處理
雖然我們實作了Generator的自動執行以及讓yield傳回resolve的值,但上邊的代碼還存在着幾點問題:
- 「需要相容基本類型」:這段代碼能自動執行的前提是
後面跟Promise,為了相容後面跟着基本類型值的情況,我們需要把yield跟的内容(yield
)都用gen().next.value
轉化一遍Promise.resolve()
- 「缺少錯誤處理」:上邊代碼裡的Promise如果執行失敗,就會導緻後續執行直接中斷,我們需要通過調用
,把錯誤抛出來,才能被外層的try-catch捕獲到Generator.prototype.throw()
- 「傳回值是Promise」:
的傳回值是一個Promise,我們這裡也需要保持一緻,給傳回值包一個Promiseasync/await
我們改造一下run方法:
function run(gen) {
//把傳回值包裝成promise
return new Promise((resolve, reject) => {
var g = gen()
function step(val) {
//錯誤處理
try {
var res = g.next(val)
} catch(err) {
return reject(err);
}
if(res.done) {
return resolve(res.value);
}
//res.value包裝為promise,以相容yield後面跟基本類型的情況
Promise.resolve(res.value).then(
val {
step(val);
},
err => {
//抛出錯誤
g.throw(err)
});
}
step();
});
}
然後我們可以測試一下:
function* myGenerator() {
try {
console.log(yield Promise.resolve(1))
console.log(yield 2) //2
console.log(yield Promise.reject('error'))
} catch (error) {
console.log(error)
}
}
const result = run(myGenerator) //result是一個Promise
//輸出 1 2 error
到這裡,一個
async/await
的實作基本完成了。最後我們可以看一下babel對async/await的轉換結果,其實整體的思路是一樣的,但是寫法稍有不同:
//相當于我們的run()
function _asyncToGenerator(fn) {
return function() {
var self = this
var args = arguments
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
//相當于我們的step()
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
//處理異常
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined);
});
};
}
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
使用方式:
const foo = _asyncToGenerator(function* () {
try {
console.log(yield Promise.resolve(1)) //1
console.log(yield 2) //2
return '3'
} catch (error) {
console.log(error)
}
})
foo().then(res {
console.log(res) //3
})
有關
async/await
的實作,到這裡告一段落。但是直到結尾,我們也不知道await到底是如何暫停執行的,有關await暫停執行的秘密,我們還要到Generator的實作中去尋找答案
Generator實作
我們從一個簡單的例子開始,一步步探究Generator的實作原理:
function* foo() {
yield 'result1'
yield 'result2'
yield 'result3'
}
const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)
我們可以在babel官網上線上轉化這段代碼,看看ES5環境下是如何實作Generator的:
"use strict";
var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);
function foo() {
return regeneratorRuntime.wrap(function foo$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 'result1';
case 2:
_context.next = 4;
return 'result2';
case 4:
_context.next = 6;
return 'result3';
case 6:
case "end":
return _context.stop();
}
}
}, _marked);
}
var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
代碼咋一看不長,但如果仔細觀察會發現有兩個不認識的東西 ——
regeneratorRuntime.mark
和
regeneratorRuntime.wrap
,這兩者其實是 regenerator-runtime 子產品裡的兩個方法,regenerator-runtime 子產品來自facebook的 regenerator 子產品,完整代碼在runtime.js,這個runtime有700多行...-_-||,是以我們不能全講,不太重要的部分我們就簡單地過一下,重點講解暫停執行相關部分代碼
❝
個人覺得啃源碼的效果不是很好,建議讀者拉到末尾先看結論和簡略版實作,源碼作為一個補充了解
❞
regeneratorRuntime.mark()
regeneratorRuntime.mark(foo)
這個方法在第一行被調用,我們先看一下runtime裡mark()方法的定義
//runtime.js裡的定義稍有不同,多了一些判斷,以下是編譯後的代碼
runtime.mark = function(genFun) {
genFun.__proto__ = GeneratorFunctionPrototype;
genFun.prototype = Object.create(Gp);
return genFun;
};
這裡邊
GeneratorFunctionPrototype
和
Gp
我們都不認識,他們被定義在runtime裡,不過沒關系,我們隻要知道
mark()方法
為生成器函數(foo)綁定了一系列原型就可以了,這裡就簡單地過了
regeneratorRuntime.wrap()
從上面babel轉化的代碼我們能看到,執行
foo()
,其實就是執行
wrap()
,那麼這個方法起到什麼作用呢,他想包裝一個什麼東西呢,我們先來看看wrap方法的定義:
//runtime.js裡的定義稍有不同,多了一些判斷,以下是編譯後的代碼
function wrap(innerFn, outerFn, self) {
var generator = Object.create(outerFn.prototype);
var context = new Context([]);
generator._invoke = makeInvokeMethod(innerFn, self, context);
return generator;
}
wrap方法先是建立了一個generator,并繼承
outerFn.prototype
;然後new了一個
context對象
;
makeInvokeMethod方法
接收
innerFn(對應foo$)
、
context
和
this
,并把傳回值挂到
generator._invoke
上;最後return了generator。「其實wrap()相當于是給generator增加了一個_invoke方法」
這段代碼肯定讓人産生很多疑問,outerFn.prototype是什麼,Context又是什麼,makeInvokeMethod又做了哪些操作。下面我們就來一一解答:
❝
其實就是
outerFn.prototype
genFun.prototype
,
❞
這個我們結合一下上面的代碼就能知道
❝
context
可以直接了解為這樣一個全局對象,用于儲存各種狀态和上下文:
❞
var ContinueSentinel = {};
var context = {
done: false,
method: "next",
next: 0,
prev: 0,
abrupt: function(type, arg) {
var record = {};
record.type = type;
record.arg = arg;
return this.complete(record);
},
complete: function(record, afterLoc) {
if (record.type === "return") {
this.rval = this.arg = record.arg;
this.method = "return";
this.next = "end";
}
return ContinueSentinel;
},
stop: function() {
this.done = true;
return this.rval;
}
};
❝
的定義如下,它return了一個
makeInvokeMethod
,invoke用于判斷目前狀态和執行下一步,其實就是我們調用的
invoke方法
next()
❞
//以下是編譯後的代碼
function makeInvokeMethod(innerFn, context) {
// 将狀态置為start
var state = "start";
return function invoke(method, arg) {
// 已完成
if (state === "completed") {
return { value: undefined, done: true };
}
context.method = method;
context.arg = arg;
// 執行中
while (true) {
state = "executing";
var record = {
type: "normal",
arg: innerFn.call(self, context) // 執行下一步,并擷取狀态(其實就是switch裡邊return的值)
};
if (record.type === "normal") {
// 判斷是否已經執行完成
state = context.done ? "completed" : "yield";
// ContinueSentinel其實是一個空對象,record.arg === {}則跳過return進入下一個循環
// 什麼時候record.arg會為空對象呢, 答案是沒有後續yield語句或已經return的時候,也就是switch傳回了空值的情況(跟着上面的switch走一下就知道了)
if (record.arg === ContinueSentinel) {
continue;
}
// next()的傳回值
return {
value: record.arg,
done: context.done
};
}
}
};
}
❝
為什麼
實際上就是
generator._invoke
gen.next
呢,因為在runtime對于next()的定義中,next()其實就return了_invoke方法
❞
// Helper for defining the .next, .throw, and .return methods of the
// Iterator interface in terms of a single ._invoke method.
function defineIteratorMethods(prototype) {
["next", "throw", "return"].forEach(function(method) {
prototype[method] = function(arg) {
return this._invoke(method, arg);
};
});
}
defineIteratorMethods(Gp);
低配實作 & 調用流程分析
這麼一遍源碼下來,估計很多讀者還是懵逼的,畢竟源碼中糾集了很多概念和封裝,一時半會不好完全了解,讓我們跳出源碼,實作一個簡單的Generator,然後再回過頭看源碼,會得到更清晰的認識
// 生成器函數根據yield語句将代碼分割為switch-case塊,後續通過切換_context.prev和_context.next來分别執行各個case
function gen$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return 'result1';
case 2:
_context.next = 4;
return 'result2';
case 4:
_context.next = 6;
return 'result3';
case 6:
case "end":
return _context.stop();
}
}
}
// 低配版context
var context = {
next:0,
prev: 0,
done: false,
stop: function stop () {
this.done = true
}
}
// 低配版invoke
let gen = function() {
return {
next: function() {
value = context.done ? undefined: gen$(context)
done = context.done
return {
value,
done
}
}
}
}
// 測試使用
var g = gen()
g.next() // {value: "result1", done: false}
g.next() // {value: "result2", done: false}
g.next() // {value: "result3", done: false}
g.next() // {value: undefined, done: true}
這段代碼并不難了解,我們分析一下調用流程:
- 我們定義的
生成器函數被轉化為以上代碼function*
- 轉化後的代碼分為三大塊:
-
由yield分割生成器函數代碼而來gen$(_context)
-
用于儲存函數執行上下文context對象
-
定義next(),用于執行gen$(_context)來跳到下一步invoke()方法
- 當我們調用
,就相當于調用g.next()
,執行invoke()方法
,進入switch語句,switch根據context的辨別,執行對應的case塊,return對應結果gen$(_context)
- 當生成器函數運作到末尾(沒有下一個yield或已經return),switch比對不到對應代碼塊,就會return空值,這時
傳回g.next()
{value: undefined, done: true}
從中我們可以看出,「Generator實作的核心在于
上下文的儲存
,函數并沒有真的被挂起,每一次yield,其實都執行了一遍傳入的生成器函數,隻是在這個過程中間用了一個context對象儲存上下文,使得每次執行生成器函數的時候,都可以從上一個執行結果開始執行,看起來就像函數被挂起了一樣」
總結 & 緻謝
有關Promise、async/await、Generator的原理就實作到這裡了,感謝大家能夠跟我一起走完全程,不知不覺,我們花了近9千字來講述有關異步程式設計的故事,異步程式設計的世界環環相扣,一開始,筆者隻是出于對await挂起機制的好奇,後來,從一個 "await是如何實作暫停執行" 的小問題,引出了對異步程式設計的一系列思考和實作原理。三者的實作,其實也是前端異步程式設計一步步演化推進的過程。
成文過程中得到很多大佬的幫助,這四篇參考文章都是我閱讀了很多相關文章後精選的四篇,建議大家結合閱讀,大佬們寫的比我好很多,另外感謝冴羽大佬在Generator機制上給予的解惑~
❝
前端技匠:各種源碼實作,你想要的這裡都有
神三元:我如何實作Promise
winty:async/await 原理及執行順序分析
冴羽:ES6 系列之 Babel 将 Generator 編譯成了什麼樣子
❞
完