原文: Write Your Own Node.js Promise Library from Scratch做者:code_barbarianhtml
Promise 已是 JavaScript 中異步處理的基石,回調的場景將會愈來愈少,並且如今能夠直接在 Node.js 使用 async/await。async/await 基於 Promise,所以須要瞭解 Promise 來掌握 async/await。這篇文章,將介紹如何編寫一個 Promise 庫,並演示如何使用 async/await。node
在 ES6 規範中,Promise 是一個類,它的構造函數接受一個 executor
函數。Promise 類的實例有一個 then()
方法。根據規範,Promise 還有其餘的一些屬性,但在這裏能夠暫時忽略,由於咱們要實現的是一個精簡版的庫。下面是一個 MyPromise
類的腳手架:bootstrap
class MyPromise { // `executor` 函數接受兩個參數,`resolve()` 和 `reject()` // 負責在異步操做成功(resolved)或者失敗(rejected)的時候調用 `resolve()` 或者 `reject()` constructor(executor) {} // 當 promise 的狀態是 fulfilled(完成)時調用 `onFulfilled` 方法, // 當 promise 的狀態是 rejected(失敗)時調用 `onRejected` 方法 // 到目前爲止,能夠認爲 'fulfilled' 和 'resolved' 是同樣的 then(onFulfilled, onRejected) {} }
executor
函數須要兩個參數,resolve()
和 reject()
。promise 是一個狀態機,包含三個狀態:數組
這樣很容易就能實現 MyPromise
構造函數的初始版本:promise
constructor(executor) { if (typeof executor !== 'function') { throw new Error('Executor must be a function') } // 初始狀態,$state 表示 promise 的當前狀態 // $chained 是當 promise 處在 settled 狀態時須要調用的函數數組 this.$state = 'PENDING' this.$chained = [] // 爲處理器函數實現 `resolve()` 和 `reject()` const resolve = res => { // 只要當 `resolve()` 或 `reject()` 被調用 // 這個 promise 對象就再也不處於 pending 狀態,被稱爲 settled 狀態 // 調用 `resolve()` 或 `reject()` 兩次,以及在 `resolve()` 以後調用 `reject()` 是無效的 if (this.$state !== 'PENDING') { return } // 後面將會談到 fulfilled 和 resolved 之間存在細微差異 this.$state = 'FULFILLED' this.$internalValue = res // If somebody called `.then()` while this promise was pending, need // to call their `onFulfilled()` function for (const { onFulfilled } of this.$chained) { onFulfilled(res) } } const reject = err => { if (this.$state !== 'PENDING') { return } this.$state = 'REJECTED' this.$internalValue = err for (const { onRejected } of this.$chained) { onRejected(err) } } // 如規範所言,調用處理器函數中的 `resolve()` 和 `reject()` try { // 若是處理器函數拋出一個同步錯誤,咱們認爲這是一個失敗狀態 // 須要注意的是,`resolve()` 和 `reject()` 只能被調用一次 executor(resolve, reject) } catch (err) { reject(err) } }
then()
函數的實現更簡單,它接受兩個參數,onFulfilled()
和 onRejected()
。then()
函數必須確保 promise 在 fulfilled 時調用 onFulfilled()
,在 rejected 時調用 onRejected()
。若是 promise 已經 resolved 或 rejected,then()
函數會當即調用 onFulfilled()
或 onRejected()
。若是 promise 仍處於 pending 狀態,就將函數推入 $chained
數組,所以後續 resolve()
和 reject()
函數仍然能夠調用它們。異步
then(onFulfilled, onRejected) { if (this.$state === 'FULFILLED') { onFulfilled(this.$internalValue) } else if (this.$state === 'REJECTED') { onRejected(this.$internalValue) } else { this.$chained.push({ onFulfilled, onRejected }) } }
*除此以外:ES6 規範表示,若是在已經 resolved 或 rejected 的 promise 調用 .then()
, 那麼 onFulfilled()
或 onRejected()
將在下一個時序被調用。因爲本文代碼只是一個教學示例而不是規範的精確實現,所以實現會忽略這些細節。async
上面的例子特地忽略了 promise 中最複雜也是最有用的部分:鏈式調用。若是 onFulfilled()
或者 onRejected()
函數返回一個 promise,則 then()
應該返回一個 「locked in」 的新 promise 以匹配這個 promise 的狀態。例如:ide
p = new MyPromise(resolve => { setTimeout(() => resolve('World'), 100) }) p .then(res => new MyPromise(resolve => resolve(`Hello, ${res}`))) // 在 100 ms 後打印 'Hello, World' .then(res => console.log(res))
下面是能夠返回 promise 的 .then()
函數實現,這樣就能夠進行鏈式調用。函數
then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { // 確保在 `onFulfilled()` 和 `onRejected()` 的錯誤將致使返回的 promise 失敗(reject) const _onFulfilled = res => { try { // 若是 `onFulfilled()` 返回一個 promise, 確保 `resolve()` 能正確處理 resolve(onFulfilled(res)) } catch (err) { reject(err) } } const _onRejected = err => { try { reject(onRejected(err)) } catch (_err) { reject(_err) } } if (this.$state === 'FULFILLED') { _onFulfilled(this.$internalValue) } else if (this.$state === 'REJECTED') { _onRejected(this.$internalValue) } else { this.$chained.push({ onFulfilled: _onFulfilled, onRejected: _onRejected }) } }) }
如今 then()
返回一個 promise,可是還須要完成一些工做:若是 onFulfilled()
返回一個 promise,resolve()
要可以正確處理。因此 resolve()
函數須要在 then()
遞歸調用,下面是更新後的 resolve()
函數:ui
const resolve = res => { // 只要當 `resolve()` 或 `reject()` 被調用 // 這個 promise 對象就再也不處於 pending 狀態,被稱爲 settled 狀態 // 調用 `resolve()` 或 `reject()` 兩次,以及在 `resolve()` 以後調用 `reject()` 是無效的 if (this.$state !== 'PENDING') { return } // 若是 `res` 是 thenable(帶有then方法的對象) // 將鎖定 promise 來保持跟 thenable 的狀態一致 if (res !== null && typeof res.then === 'function') { // 在這種狀況下,這個 promise 是 resolved,可是仍處於 'PENDING' 狀態 // 這就是 ES6 規範中說的"一個 resolved 的 promise",可能處在 pending, fulfilled 或者 rejected 狀態 // http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects return res.then(resolve, reject) } this.$state = 'FULFILLED' this.$internalValue = res // If somebody called `.then()` while this promise was pending, need // to call their `onFulfilled()` function for (const { onFulfilled } of this.$chained) { onFulfilled(res) } return res }
爲了簡單起見,上面的例子省略了一旦 promise 被鎖定用以匹配另外一個 promise 時,調用 resolve() 或者 reject() 是無效的關鍵細節。在上面的例子中,你能夠 resolve() 一個 pending 的 promise ,而後拋出一個錯誤,而後 res.then(resolve, reject) 將會無效。這僅僅是一個例子,而不是 ES6 promise 規範的徹底實現。
上面的代碼說明了 resolved 的 promise 和 fulfilled 的 promise 之間的區別。這種區別是微妙的,而且與 promise 鏈式調用有關。resolved 不是一種真正的 promise 狀態,但它是ES6規範中定義的術語。當對一個已經 resolved 的 promise 調用 resolve()
,可能會發生如下兩件事之一:
resolve(v)
時,若是 v
不是一個 promise ,那麼 promise 當即成爲 fulfilled。在這種簡單的狀況下,resolved 和 fulfilled 就是同樣的。resolve(v)
時,若是 v
是另外一個 promise,那麼這個 promise 一直處於 pending 直到 v
調用 resolve 或者 reject。在這種狀況下, promise 是 resolved 但處於 pending 狀態。關鍵字 await
會暫停執行一個 async
函數,直到等待的 promise 變成 settled 狀態。如今咱們已經有了一個簡單的自制 promise 庫,看看結合使用 async/await 中時會發生什麼。向 then()
函數添加一個 console.log()
語句:
then(onFulfilled, onRejected) { console.log('Then', onFulfilled, onRejected, new Error().stack) return new MyPromise((resolve, reject) => { /* ... */ }) }
如今,咱們來 await
一個 MyPromise
的實例,看看會發生什麼。
run().catch(error => console.error(error.stack)) async function run() { const start = Date.now() await new MyPromise(resolve => setTimeout(() => resolve(), 100)) console.log('Elapsed time', Date.now() - start) }
注意上面的 .catch()
調用。catch()
函數是 ES6 promise 規範的核心部分。本文不會詳細講述它,由於 .catch(f)
至關於 .then(null, f)
,沒有什麼特別的內容。
如下是輸出內容,注意 await 隱式調用 .then()
中的 onFulfilled()
和 onRejected()
函數,這是 V8 底層的 C++ 代碼(native code)。此外,await
會一直等待調用 .then()
直到下一個時序。
Then function () { [native code] } function () { [native code] } Error at MyPromise.then (/home/val/test/promise.js:63:50) at process._tickCallback (internal/process/next_tick.js:188:7) at Function.Module.runMain (module.js:686:11) at startup (bootstrap_node.js:187:16) at bootstrap_node.js:608:3 Elapsed time 102
async/await 是很是強大的特性,但掌握起來稍微有點困難,由於須要使用者瞭解 promise 的基本原則。 promise 有不少細節,例如捕獲處理器函數中的同步錯誤,以及 promise 一旦解決就沒法改變狀態,這使得 async/await 成爲可能。一旦對 promise 有了充分的理解,async/await 就會變得容易得多。