筆者剛接觸async/await
時,就被其暫停執行的特性吸引了,心想在沒有原生API支持的狀況下,await竟然能掛起當前方法,實現暫停執行,我感到十分好奇。好奇心驅使我一層一層剝開有關JS異步編程的一切。閱讀完本文,讀者應該可以瞭解:前端
Promise
的實現原理async/await
的實現原理Generator
的實現原理在成文過程當中,筆者查閱了不少講解Promise實現的文章,但感受大多文章都很難稱得上條理清晰,有的上來就放大段Promise規範翻譯,有的在Promise基礎使用上浪費篇幅,又或者把一個簡單的東西長篇大論,過分講解,我推薦頭鐵的同窗直接拉到本章小結看最終實現,結合着註釋直接啃代碼也能理解十之八九react
迴歸正題,文章開頭咱們先點一下Promise爲咱們解決了什麼問題:在傳統的異步編程中,若是異步之間存在依賴關係,咱們就須要經過層層嵌套回調來知足這種依賴,若是嵌套層數過多,可讀性和可維護性都變得不好,產生所謂「回調地獄」,而Promise將回調嵌套改成鏈式調用,增長可讀性和可維護性。下面咱們就來一步步實現一個Promise:git
咱們先來看一個最簡單的Promise使用:github
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result')
},
1000);
})
p1.then(res => console.log(res), err => console.log(err))
複製代碼
觀察這個例子,咱們分析Promise的調用流程:編程
Promise
的構造方法接收一個executor()
,在new Promise()
時就馬上執行這個executor回調executor()
內部的異步任務被放入宏/微任務隊列,等待執行then()
被執行,收集成功/失敗回調,放入成功/失敗隊列executor()
的異步任務被執行,觸發resolve/reject
,從成功/失敗隊列中取出回調依次執行其實熟悉設計模式的同窗,很容易就能意識到這是個觀察者模式,這種收集依賴 -> 觸發通知 -> 取出依賴執行
的方式,被普遍運用於觀察者模式的實現,在Promise裏,執行順序是then收集依賴 -> 異步觸發resolve -> resolve執行依賴
。依此,咱們能夠勾勒出Promise的大體形狀:redux
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:promise
上面咱們已經簡單地實現了一個超低配版Promise,但咱們會看到不少文章和咱們寫的不同,他們的Promise實現中還引入了各類狀態控制,這是因爲ES6的Promise實現須要遵循Promise/A+規範,是規範對Promise的狀態控制作了要求。Promise/A+的規範比較長,這裏只總結兩條核心規則:bash
根據規範,咱們補充一下Promise的代碼:
- Promise本質是一個狀態機,且狀態只能爲如下三種:
Pending(等待態)
、Fulfilled(執行態)
、Rejected(拒絕態)
,狀態的變動是單向的,只能從Pending -> Fulfilled 或 Pending -> Rejected,狀態變動不可逆then方法
接收兩個可選參數,分別對應狀態改變時觸發的回調。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)
}
}
複製代碼
補充完規範,咱們接着來實現鏈式調用,這是Promise實現的重點和難點,咱們先來看一下then是如何鏈式調用的:babel
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
複製代碼
咱們思考一下如何實現這種鏈式調用:
.then()
須要返回一個Promise,這樣才能找到then方法,因此咱們會把then方法的返回值包裝成Promise。.then()
的回調須要拿到上一個.then()
的返回值.then()
的回調須要順序執行,以上面這段代碼爲例,雖然中間return了一個Promise,但執行順序仍要保證是1->2->3。咱們要等待當前Promise狀態變動後,再執行下一個then收集的回調,這就要求咱們對then的返回值分類討論// then方法
then(resolveFn, rejectFn) {
//return一個新的promise
return new MyPromise((resolve, reject) => {
//把resolveFn從新包裝一下,再push進resolve執行隊列,這是爲了可以獲取回調的返回值進行分類討論
const fulfilledFn = value => {
try {
//執行第一個(當前的)Promise的成功回調,並獲取返回值
let x = resolveFn(value)
//分類討論返回值,若是是Promise,那麼等待Promise狀態變動,不然直接resolve
//這裏resolve以後,就能被下一個.then()的回調獲取到返回值,從而實現鏈式調用
x instanceof MyPromise ? 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 MyPromise ? 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
複製代碼
咱們已經初步完成了鏈式調用,可是對於 then() 方法,咱們還要兩個細節須要處理一下
padding
的狀況,可是有些時候,resolve/reject 在 then() 以前就被執行(好比Promise.resolve().then()
),若是這個時候還把then()回調push進resolve/reject的執行隊列裏,那麼回調將不會被執行,所以對於狀態已經變爲fulfilled
或rejected
的狀況,咱們直接執行then回調:// then方法,接收一個成功的回調和一個失敗的回調
then(resolveFn, rejectFn) {
// 根據規範,若是then的參數不是function,則咱們須要忽略它, 讓鏈式調用繼續往下執行
typeof resolveFn !== 'function' ? resolveFn = value => value : null
typeof rejectFn !== 'function' ? rejectFn = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
// return一個新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn從新包裝一下,再push進resolve執行隊列,這是爲了可以獲取回調的返回值進行分類討論
const fulfilledFn = value => {
try {
// 執行第一個(當前的)Promise的成功回調,並獲取返回值
let x = resolveFn(value)
// 分類討論返回值,若是是Promise,那麼等待Promise狀態變動,不然直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? 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;
}
})
}
複製代碼
完成了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 = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
// return一個新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn從新包裝一下,再push進resolve執行隊列,這是爲了可以獲取回調的返回值進行分類討論
const fulfilledFn = value => {
try {
// 執行第一個(當前的)Promise的成功回調,並獲取返回值
let x = resolveFn(value)
// 分類討論返回值,若是是Promise,那麼等待Promise狀態變動,不然直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? 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的主要功能(`∀´)Ψ
剩下的幾個方法都很是簡單,咱們順手收拾掉:
catch()方法
返回一個Promise,而且處理拒絕的狀況。它的行爲與調用Promise.prototype.then(undefined, onRejected) 相同。
//catch方法其實就是執行一下then的第二個回調
catch(rejectFn) {
return this.then(undefined, rejectFn)
}
複製代碼
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同理
)
}
複製代碼
PS. 有同窗問我MyPromise.resolve(callback())
的意義,這裏補充解釋一下:這個寫法其實涉及到一個finally()
的使用細節,finally()若是return了一個reject狀態的Promise,將會改變當前Promise的狀態,這個MyPromise.resolve
就用於改變Promise狀態,在finally()沒有返回reject態Promise或throw錯誤的狀況下,去掉MyPromise.resolve
也是同樣的(歡迎你們向我提問,勘誤的過程當中也能很好地加深本身對Promise的理解,你們能夠在各個交流羣裏直接@我)
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方法
static reject(reason) {
return new MyPromise((resolve, reject) => reject(reason))
}
複製代碼
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(iterable)
方法返回一個 promise,一旦迭代器中的某個promise解決或拒絕,返回的 promise就會解決或拒絕。
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
//同時執行Promise,若是有一個Promise的狀態發生改變,就變動新MyPromise的狀態
for (let p of promiseArr) {
MyPromise.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 = reason => {
throw new Error(reason instanceof Error? reason.message:reason);
} : null
// return一個新的promise
return new MyPromise((resolve, reject) => {
// 把resolveFn從新包裝一下,再push進resolve執行隊列,這是爲了可以獲取回調的返回值進行分類討論
const fulfilledFn = value => {
try {
// 執行第一個(當前的)Promise的成功回調,並獲取返回值
let x = resolveFn(value)
// 分類討論返回值,若是是Promise,那麼等待Promise狀態變動,不然直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (error) {
reject(error)
}
}
// reject同理
const rejectedFn = error => {
try {
let x = rejectFn(error)
x instanceof MyPromise ? 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) {
MyPromise.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使用細節從新學習的過程。
雖然前邊花了這麼多篇幅講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
看起來其實已經很類似了,它們都提供了暫停執行的功能,但兩者又有三點不一樣:
async/await
自帶執行器,不須要手動調用next()就能自動執行下一步async
函數返回值是Promise對象,而Generator返回的是生成器對象await
可以返回Promise的resolve/reject的值咱們對async/await的實現,其實也就是對應以上三點封裝Generator
咱們先來看一下,對於這樣一個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()獲取到的都是最新的迭代器,所以獲取迭代器操做要放在_next()以前,不然會進入死循環
function _next(val) { //封裝一個方法, 遞歸執行g.next()
var res = g.next(val) //獲取迭代器對象,並返回resolve的值
if(res.done) return res.value //遞歸終止條件
res.value.then(val => { //Promise的then方法是實現自動迭代的前提
_next(val) //等待Promise完成就自動執行下一個next,並傳入resolve的值
})
}
_next() //第一次執行
}
複製代碼
對於咱們以前的例子,咱們就能這樣執行:
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方法裏咱們把執行下一步的操做封裝成_next()
,每次Promise.then()的時候都去執行_next()
,實現自動迭代的效果。在迭代的過程當中,咱們還把resolve的值傳入gen.next()
,使得yield得以返回Promise的resolve的值
這裏插一句,是否是隻有
.then方法
這樣的形式才能完成咱們自動執行的功能呢?答案是否認的,yield後邊除了接Promise,還能夠接thunk函數
,thunk函數不是一個新東西,所謂thunk函數,就是單參的只接受回調的函數,詳細介紹能夠看阮一峯Thunk 函數的含義和用法,不管是Promise仍是thunk函數,其核心都是經過傳入回調的方式來實現Generator的自動執行。thunk函數只做爲一個拓展知識,理解有困難的同窗也能夠跳過這裏,並不影響後續理解。
雖然咱們實現了Generator的自動執行以及讓yield返回resolve的值,但上邊的代碼還存在着幾點問題:
yield
後面跟Promise,爲了兼容後面跟着基本類型值的狀況,咱們須要把yield跟的內容(gen().next.value
)都用Promise.resolve()
轉化一遍Generator.prototype.throw()
,把錯誤拋出來,才能被外層的try-catch捕獲到async/await
的返回值是一個Promise,咱們這裏也須要保持一致,給返回值包一個Promise咱們改造一下run方法:
function run(gen) {
//把返回值包裝成promise
return new Promise((resolve, reject) => {
var g = gen()
function _next(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 => {
_next(val);
},
err => {
//拋出錯誤
g.throw(err)
});
}
_next();
});
}
複製代碼
而後咱們能夠測試一下:
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,和async保持一致。咱們的run直接執行了Generator,實際上是不太規範的
return function() {
var self = this
var args = arguments
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
//至關於咱們的_next()
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(foo)
這個方法在第一行被調用,咱們先看一下runtime裏mark()方法的定義
//runtime.js裏的定義稍有不一樣,多了一些判斷,如下是編譯後的代碼
runtime.mark = function(genFun) {
genFun.__proto__ = GeneratorFunctionPrototype;
genFun.prototype = Object.create(Gp);
return genFun;
};
複製代碼
這裏邊GeneratorFunctionPrototype
和Gp
咱們都不認識,他們被定義在runtime裏,不過不要緊,咱們只要知道mark()方法
爲生成器函數(foo)綁定了一系列原型就能夠了,這裏就簡單地過了
從上面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;
}
};
複製代碼
makeInvokeMethod
的定義以下,它return了一個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*
生成器函數被轉化爲以上代碼gen$(_context)
由yield分割生成器函數代碼而來context對象
用於儲存函數執行上下文invoke()方法
定義next(),用於執行gen$(_context)來跳到下一步g.next()
,就至關於調用invoke()方法
,執行gen$(_context)
,進入switch語句,switch根據context的標識,執行對應的case塊,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 編譯成了什麼樣子
最後卑微求個贊Thanks♪(・ω・)ノ
往期文章