天天看點

【JS】548- Promise/async/Generator實作原了解析

【JS】548- Promise/async/Generator實作原了解析

筆者剛接觸​

​async/await​

​時,就被其暫停執行的特性吸引了,心想在沒有原生API支援的情況下,await居然能挂起目前方法,實作暫停執行,我感到十分好奇。好奇心驅使我一層一層剝開有關JS異步程式設計的一切。閱讀完本文,讀者應該能夠了解:

  1. ​Promise​

    ​的實作原理
  2. ​async/await​

    ​的實作原理
  3. ​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()​

    ​,在​

    ​new Promise()​

    ​時就立刻執行這個executor回調
  • ​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+的規範比較長,這裡隻總結兩條核心規則:

  1. Promise本質是一個狀态機,且狀态隻能為以下三種:​

    ​Pending(等待态)​

    ​、​

    ​Fulfilled(執行态)​

    ​、​

    ​Rejected(拒絕态)​

    ​,狀态的變更是單向的,隻能從Pending -> Fulfilled 或 Pending -> Rejected,狀态變更不可逆
  2. ​then方法​

    ​接收兩個可選參數,分别對應狀态改變時觸發的回調。then方法傳回一個promise。then 方法可以被同一個 promise 調用多次。
【JS】548- Promise/async/Generator實作原了解析

根據規範,我們補充一下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      

我們思考一下如何實作這種鍊式調用:

  1. 顯然​

    ​.then()​

    ​需要傳回一個Promise,這樣才能找到then方法,是以我們會把then方法的傳回值包裝成Promise。
  2. ​.then()​

    ​的回調需要順序執行,以上面這段代碼為例,雖然中間return了一個Promise,但執行順序仍要保證是1->2->3。我們要等待目前Promise狀态變更後,再執行下一個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() 方法,我們還要兩個細節需要處理一下

  1. 「值穿透」:根據規範,如果 then() 接收的參數不是function,那麼我們應該忽略它。如果沒有忽略,當then()回調不為function時将會抛出異常,導緻鍊式調用中斷
  2. 「處理狀态為resolve/reject的情況」:其實我們上邊 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 = 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​

​看起來其實已經很相似了,它們都提供了暫停執行的功能,但二者又有三點不同:

  • ​async/await​

    ​自帶執行器,不需要手動調用next()就能自動執行下一步
  • ​async​

    ​函數傳回值是Promise對象,而Generator傳回的是生成器對象
  • ​await​

    ​能夠傳回Promise的resolve/reject的值

我們對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的值

這裡插一句,是不是隻有​

​.then方法​

​​這樣的形式才能完成我們自動執行的功能呢?答案是否定的,yield後邊除了接Promise,還可以接​

​thunk函數​

​,thunk函數不是一個新東西,所謂thunk函數,就是「單參的隻接受回調的函數」,詳細介紹可以看阮一峰Thunk 函數的含義和用法,無論是Promise還是thunk函數,其核心都是通過「傳入回調」的方式來實作Generator的自動執行。thunk函數隻作為一個拓展知識,了解有困難的同學也可以跳過這裡,并不影響後續了解。

2.傳回Promise & 異常處理​

雖然我們實作了Generator的自動執行以及讓yield傳回resolve的值,但上邊的代碼還存在着幾點問題:

  1. 「需要相容基本類型」:這段代碼能自動執行的前提是​

    ​yield​

    ​後面跟Promise,為了相容後面跟着基本類型值的情況,我們需要把yield跟的内容(​

    ​gen().next.value​

    ​)都用​

    ​Promise.resolve()​

    ​轉化一遍
  2. 「缺少錯誤處理」:上邊代碼裡的Promise如果執行失敗,就會導緻後續執行直接中斷,我們需要通過調用​

    ​Generator.prototype.throw()​

    ​,把錯誤抛出來,才能被外層的try-catch捕獲到
  3. 「傳回值是Promise」:​

    ​async/await​

    ​的傳回值是一個Promise,我們這裡也需要保持一緻,給傳回值包一個Promise

我們改造一下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;
  }
};      

​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}      

這段代碼并不難了解,我們分析一下調用流程:

  1. 我們定義的​

    ​function*​

    ​生成器函數被轉化為以上代碼
  2. 轉化後的代碼分為三大塊:
  • ​gen$(_context)​

    ​由yield分割生成器函數代碼而來
  • ​context對象​

    ​用于儲存函數執行上下文
  • ​invoke()方法​

    ​定義next(),用于執行gen$(_context)來跳到下一步
  • 當我們調用​

    ​g.next()​

    ​,就相當于調用​

    ​invoke()方法​

    ​,執行​

    ​gen$(_context)​

    ​,進入switch語句,switch根據context的辨別,執行對應的case塊,return對應結果
  • 當生成器函數運作到末尾(沒有下一個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   編譯成了什麼樣子

繼續閱讀