從零開始寫一個 Promise 庫

原文: 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

Promise 是什麼?

在 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 是一個狀態機,包含三個狀態:數組

  • pending:初始狀態,既不是成功,也不是失敗狀態
  • fulfilled:意味着操做成功完成,返回結果值
  • rejected:意味着操做失敗,返回錯誤信息

這樣很容易就能實現 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 調用鏈

上面的例子特地忽略了 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 狀態。

與 Async/Await 一塊兒使用

關鍵字 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 就會變得容易得多。

相關文章
相關標籤/搜索