手撕源碼系列 —— 函子 + 觀察者模式 + 狀態 = Promise

前言

前段時間太忙,隔了快一個月沒寫博客,可是 Promise 其實很早以前就已經總結了一波如何實現,可是那個時候純粹是爲了實現而實現,沒有去細品其中的一些巧妙設計,直到最近在進行函數式編程相關的知識學習時,無心中在查閱資料的時候發現,PromiseFunctor 竟然有着千絲萬縷的關係,這讓我決定要從新審視一下本身對 Promise 的認知,因而便有了這篇「老酒新裝」的博客。javascript

前置知識

想要徹底閱讀並理解這篇博客,我粗略地估算了一下,大概須要如下的一些前置知識,不瞭解的同窗能夠自行先去學習一下:前端

  • 遞歸的思想
  • ES6Typescript 的基礎認知
  • 函數式編程中的函子(Functor)和「函數是一等公民」思想
  • 設計模式中的觀察者模式

分步實現 Promise

咱們能夠將實現一個 Promise 比做蓋一棟樓,經過拆分每一步並解決、理解和記憶,達到很快就能理解它的實現的目的。java

1、打下 Promise 的地基

因爲咱們經常使用 Promise 配合 async/await 來實現異步編程,因此先回想一下如何最最基本地使用 Promisegit

new Promise(resolve => {...})
    .then(onFulfilled)
    .then(onFulfilled)
複製代碼

根據上面咱們回憶中的最最基本的 Promise 咱們能夠寫出如下實現:github

class Promise {
    constructor(executor) {
        const resolve = () => {}
        
        executor(resolve)
    }
    
    then = onFulfilled => new Promise(...)
}
複製代碼

好的,那咱們的第一步到這裏就結束了,是否是十分簡單,徹底不須要什麼成本就能理解並記憶下來。shell

2、加入原材料 函子 和 觀察者模式

這一步,咱們開始往裏加東西了。對於加的東西,咱們也必須瞭解是什麼,因此先來看下兩個原材料的基本概念。npm

函子

因爲在 前置知識 裏提到,相信你們已經對它有所瞭解,一個基本的函子咱們能夠寫成如下實現:編程

class Functor {
    static of = value => new Functor(this.value)
    
    constructor(value) {
        this.value = value
    }
    
    map = fn => Functor.of(fn(this.value))
}
複製代碼

爲了方便映射到 Promise 的實現中,改成如下寫法:設計模式

class Functor {
    constructor(value) {
        this.value = value
    }
    
    map = fn => new Functor(fn(this.value))
}
複製代碼

而後結合到函子的一些特性:數組

const plus = x + y => x + y
const plusOne = x => plus(x, 1)

new Functor(100)
    .map(plusOne)
    .map(plusOne) // { value: 102 }
複製代碼

這個是時候再發揮咱們從小被培養的找規律能力,咱們發現:

  1. 二者的結構相似,擁有一個方法對內部的數據進行操做
  2. 二者都可進行鏈式調用

經過以上兩點能夠獲得一個結論,之因此引入函子,能夠解決鏈式調用的問題,可是光有一個函子不夠呀,函子只能實現同步的鏈式調用,這時候另一個原材料觀察者模式就出場了。

觀察者模式

先看一個簡單的觀察者模式實現:

class Observer {
  constructor() {
    this.callbacks = []

    this.notify = value => {
      this.callbacks.forEach(observe => observe(value))
    }
  }

  subscribe = observer => {
    this.callbacks.push(observer)
  }
}
複製代碼

這時候聰明的人一下就發現了,這個 notifysubscribe 不就是 resolvethen 嘛!

俺のターン!ドロー!魔法発動!

ターンエンド!

class Promise {
    constructor(executor) {
        this.value = undefined
        this.callbacks = []
        
        // 至關於 notify
        const resolve = value => {
            this.value = value
            this.callbacks.forEach(callback => callback())
        }
        
        executor(resolve)
    }
    
    // 至關於 subscribe 或 map
    then = onFulfilled => new Promise(resolve => {
        this.callbacks.push(() => resolve(onFulfilled(this.value)))
    })
}
複製代碼

融合後的初級 Promise 已經具備異步鏈式調用的能力了好比:

const promise = new Promise(resolve => {
    setTimeout(() => {
        resolve(100)
    }, 500)
})
    .map(plusOne)
    .map(plusOne)
    // { value: 102 }
複製代碼

可是當咱們進行一些騷操做時,依然會出問題:

const promise = new Promise(resolve => {
    setTimeout(() => {
        resolve(100)
        resolve(1000)
    }, 500)
})
    .map(plusOne)
    .map(plusOne)
    // { value: 1002 }
複製代碼

爲了解決這個問題,咱們還須要一個原材料狀態

篇幅有限,這一部分更細緻的轉換過程,個人 repo 都有記錄。

3、加入原材料 狀態

衆所周知,「青眼究極龍鬚要三條青眼白龍」,爲了解決上一部分留下的問題,這一部分,須要給 Promise 加入狀態這個原材料。

class Promise {
  static PENDING = 'PENDING'
  static FULFILLED = 'FULFILLED'

  constructor(executor) {
    this.value = undefined
    this.callbacks = []
    this.status = Promise.PENDING

    // 一系列操做(狀態的改變,成功回調的執行)
    const resolve = value => {
      // 只有處於 pending 狀態的 promise 能調用 resolve
      if (this.status === Promise.PENDING) {
        // resolve 調用後,status 轉爲 fulfilled
        this.status = Promise.FULFILLED
        // 儲存 fulfilled 的終值
        this.value = value
        // 一旦 resolve 執行,調用儲存在回調數組裏的回調
        this.callbacks.forEach(callback => callback())
      }
    }

    executor(resolve)
  }

  then = onFulfilled =>
    new Promise(resolve => {
      // 當 status 爲執行態(Fulfilled)時
      if (this.status === Promise.FULFILLED) {
        resolve(onFulfilled(this.value))
      }
      // 當 status 爲 Pending 時
      if (this.status === Promise.PENDING) {
        // 將 onFulfilled 存入回調數組
        this.callbacks.push(() => resolve(onFulfilled(this.value)))
      }
    })
}
複製代碼

至此,經過三大原材料構建出的 Promise 就完成了,固然,還有不少功能沒有實現,魯迅曾經說過:「要站在巨人的肩膀上看問題。」,下一步,就須要 Promise/A+ 規範來來幫助咱們實現一個具備完整功能的 Promise

4、打開設計圖紙 Promise/A+ 規範

劍來! Promise/A+ 規範,接下來的操做,須要跟着它一步一步進行。

一、加入拒絕態以及處理(reject and onRejected)

其實這一步不用規範咱們也知道,Promise 擁有的終態fulfilledrejected 兩種,因此要把剩下的 rejected 以及一些相關操做給補上。

class Promise {
  ......
  static REJECTED = 'REJECTED'
  
  constructor(executor) {
    this.value = undefined
    this.reason = undefined
    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []
    this.status = PromiseFunctorWithTwoStatus.PENDING

    // 成功後的一系列操做(狀態的改變,成功回調的執行)
    const resolve = value => {
        ......
    }
    
    // 失敗後的一系列操做(狀態的改變,失敗回調的執行)
    const reject = reason => {
      // 只有處於 pending 狀態的 promise 能調用 resolve
      if (this.status === Promise.PENDING) {
        // reject 調用後,status 轉爲 rejected
        this.status = Promise.REJECTED
        // 儲存 rejected 的拒因
        this.reason = reason
        // 一旦 reject 執行,調用儲存在失敗回調數組裏的回調
        this.onRejectedCallbacks.forEach(onRejected => onRejected())
      }
    }

    executor(resolve, reject)
  }

  then = (onFulfilled, onRejected) =>
    new Promise(resolve => {
      // 當 status 爲執行態(Fulfilled)時
      ......
      
      // 當 status 爲拒絕態(Rejected)時
      if (this.status === PromiseFunctorWithTwoStatus.REJECTED) {
        reject(onRejected(this.reason))
      }
      
      // 當 status 爲 Pending 時
      if (this.status === Promise.PENDING) {
        // 將 onFulfilled 存入回調數組
        this.onFulfilledCallbacks.push(() => resolve(onFulfilled(this.value)))
        // 將 onRejected 存入失敗回調數組
        this.onRejectedCallbacks.push(() => reject(onRejected(this.reason)))
      }
    })
}
複製代碼

二、加入核心 resolvePromise 方法實現解決過程

Promise 解決過程是一個抽象的操做,其需輸入一個 promise 和一個值,咱們表示爲 [[Resolve]](promise, x),若是 x 有 then 方法且看上去像一個 Promise ,解決程序即嘗試使 promise 接受 x 的狀態;不然其用 x 的值來執行 promise 。

這種 thenable 的特性使得 Promise 的實現更具備通用性:只要其暴露出一個遵循 Promise/A+ 協議的 then 方法便可;這同時也使遵循 Promise/A+ 規範的實現能夠與那些不太規範但可用的實現能良好共存。

根據規範的描述,咱們依照他給的實現步驟,寫出代碼實現:

class Promise {
    ......
    static resolvePromise = (anotherPromise, x, resolve, reject) => {
      // 若是 onFulfilled 或者 onRejected 返回一個值 x ,則運行下面的 Promise 解決過程:[[Resolve]](promise2, x)
      // 運行 [[Resolve]](promise, x) 需遵循如下步驟:
        
      // 若是 promise 和 x 指向同一對象,以 TypeError 爲拒因拒絕執行 promise 以防止循環引用
      if (anotherPromise === x) {
        return reject(new TypeError('Chaining cycle detected for promise'))
      }
      
      // 若是 x 爲 Promise ,則使 promise 接受 x 的狀態
      if (x instanceof Promise) {
          x.then(
          // 若是 x 處於執行態,用相同的值執行 promise
          value => {
            return Promise.resolvePromise(anotherPromise, value, resolve, reject)
          },
          // 若是 x 處於拒絕態,用相同的拒因拒絕 promise
          reason => {
            return reject(reason)
          }
        )
        // 若是 x 爲對象或者函數
      } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
          let called = false
          try {
              // 把 x.then 賦值給 then(這步咱們先是存儲了一個指向 x.then 的引用,而後測試並調用該引用,以免屢次訪問 x.then 屬性。這種預防措施確保了該屬性的一致性,由於其值可能在檢索調用時被改變。)
              const then = x.then
              // 若是 then 是函數,將 x 做爲函數的做用域 this 調用之。傳遞兩個回調函數做爲參數,
              if (typeof then === 'function') {
                then.call(
                    x,
                    // 第一個參數叫作 resolvePromise ,
                    value => {
                        // 若是 resolvePromise 和 rejectPromise 均被調用,或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用
                        if (called) {
                            return 
                        }
                        called = true
                        // 若是 resolvePromise 以值 y 爲參數被調用,則運行 [[Resolve]](promise, y)
                        return Promise.resolvePromise(
                          anotherPromise,
                          value,
                          resolve,
                          reject
                        )
                    },
                    // 第二個參數叫作 rejectPromise
                    reason => {
                        // 若是 resolvePromise 和 rejectPromise 均被調用,或者被同一參數調用了屢次,則優先採用首次調用並忽略剩下的調用
                        if (called) {
                          return
                        }
                        called = true
                        // 若是 rejectPromise 以拒因 r 爲參數被調用,則以拒因 r 拒絕 promise
                        return reject(reason)
                    }
                )
              } else {
                  //若是 then 不是函數,以 x 爲參數執行 promise
                  return resolve(x)
              }
          } catch (error) {
              // 若是調用 then 方法拋出了異常 e, 若是 resolvePromise 或 rejectPromise 已經被調用,則忽略之
              if (called) {
                  return
              }
              called = true
              // 若是取 x.then 的值時拋出錯誤 e ,則以 e 爲拒因拒絕 promise
              return reject(error)
          }
      } else {
          // 若是 x 不爲對象或者函數,以 x 爲參數執行 promise
          return resolve(x)
      }
    }
    ......
}
複製代碼

同時,咱們要對以前的 Promise 裏的 resolve 方法進行改造:

class Promise {
    ......
    constructor(executor) {
        ......
        // 成功後的一系列操做(狀態的改變,成功回調的執行)
        const resolve = x => {
          const __resolve = value => {
            // 只有處於 pending 狀態的 promise 能調用 resolve
            if (this.status === Promise.PENDING) {
              // resolve 調用後,status 轉爲 fulfilled
              this.status = Promise.FULFILLED
              // 儲存 fulfilled 的終值
              this.value = value
              // 一旦 resolve 執行,調用儲存在成功回調數組裏的回調
              this.onFulfilledCallbacks.forEach(onFulfilled => onFulfilled())
            }
          }
          return Promise.resolvePromise.call(this, this, x, __resolve, reject)
        }
        ......
    }
    ......
}
複製代碼
class Promise {
    ......
    then = (onFulfilled, onRejected) => {
        // then 方法必須返回一個 promise 對象
        const anotherPromise = new Promise((resolve, reject) => {
          // 封裝處理鏈式調用的方法
          const handle = (fn, argv) => {
            // 確保 onFulfilled 和 onRejected 方法異步執行
            setTimeout(() => {
              try {
                const x = fn(argv)
                return Promise.resolvePromise(anotherPromise, x, resolve, reject)
              } catch (error) {
                return reject(error)
              }
            })
          }
          // 當 status 爲執行態(Fulfilled)時
          if (this.status === Promise.FULFILLED) {
            // 則執行 onFulfilled,value 做爲第一個參數
            handle(onFulfilled, this.value)
          }
          // 當 status 爲拒絕態(Rejected)時
          if (this.status === Promise.REJECTED) {
            // 則執行 onRejected,reason 做爲第一個參數
            handle(onRejected, this.reason)
          }
    
          // 當 status 爲 Pending 時
          if (this.status === Promise.PENDING) {
            // 將 onFulfilled 存入成功回調數組
            this.onFulfilledCallbacks.push(() => {
              handle(onFulfilled, this.value)
            })
            // 將 onRejected 存入失敗回調數組
            this.onRejectedCallbacks.push(() => {
              handle(onRejected, this.reason)
            })
          }
        })
    
        return anotherPromise
    }
    ......
}
複製代碼

三、加入 其它方法 完善周邊

Promise 的主體已經寫好了,接下來要實現其餘的一些輔助方法來完善它。

  • catch
catch = onRejected => {
    return this.then(null, onRejected)
}
複製代碼
  • finally
finally = fn => {
    return this.then(
        value => {
            setTimeout(fn)
            return value
        },
        reason => {
            setTimeout(fn)
            throw reason
        }
    )
}
複製代碼
  • resolve
static resolve = value => new Promise((resolve, reject) => resolve(value))
複製代碼
  • reject
static reject = reason => new Promise((resolve, reject) => reject(reason))
複製代碼
  • all
static all = promises => {
    if (!isArrayLikeObject(promises)) {
      throw new TypeError(
        `${ typeof promises === 'undefined' ? '' : typeof promises } ${promises} is not iterable (cannot read property Symbol(Symbol.iterator))`
      )
    }
    
    // 實現的 promise 基於 macroTask 的 setTimeout 實現,須要 async/await 調節執行順序
    // 原生的 promise 基於 microTask 實現,執行順序是正確的,不須要 async/await
    return new Promise(async (resolve, reject) => {
      const result = []

      for (const promise of promises) {
        await Promise.resolve(promise).then(resolvePromise, rejectPromise)
      }

      return resolve(result)

      function resolvePromise(value) {
        if (value instanceof Promise) {
          value.then(resolvePromise, rejectPromise)
        } else {
          result.push(value)
        }
      }

      function rejectPromise(reason) {
        return reject(reason)
      }
    })
}
複製代碼
  • race
static race = promises => {
    if (!isArrayLikeObject(promises)) {
      throw new TypeError(
        `${ typeof promises === 'undefined' ? '' : typeof promises } ${promises} is not iterable (cannot read property Symbol(Symbol.iterator))`
      )
    }
    
    return new Promise((resolve, reject) => {
      for (const promise of promises) {
        Promise.resolve(promise).then(
          value => resolve(value),
          reason => reject(reason)
        )
      }
    })
}
複製代碼

四、加入一些健壯性代碼

這一部分基本上屬於修修補補了,增強 Promise 的健壯性

  • 校驗 executor
constructor(executor) {
    // 參數校驗
    if (typeof executor !== 'function') {
      throw new TypeError(`Promise resolver ${executor} is not a function`)
    }
}
複製代碼
  • 利用 Maybe函子 的思想,校驗 onFulfilledonRejected
then = (onFulfilled, onRejected) => {
    // 若是 onFulfilled 不是函數,其必須被「忽略」
    onFulfilled =
      typeof onFulfilled === 'function' ? onFulfilled : value => value

    // 若是 onFulfilled 不是函數,其必須被「忽略」
    onRejected =
      typeof onRejected === 'function'
        ? onRejected
        : error => {
            throw error
          }
}
複製代碼

5、裝修爲 Typescript 風格

這一部分就不寫上來了,repo 裏有記錄。

6、測試是否符合 Promise/A+ 規範

咱們經過一個庫來檢測寫好的 Promise

添加須要的膠水代碼:

class Promise {
    ......
    static defer = () => {
        let dfd: any = {}
        dfd.promise = new Promise((resolve, reject) => {
          dfd.resolve = resolve
          dfd.reject = reject
        })
        return dfd
    }

    static deferred = Promise.defer
    ......
}
複製代碼
npm i promises-aplus-tests -D

npx promises-aplus-tests promise.js
複製代碼

總結

最近在翻閱資料的過程當中,真實地感悟到什麼是「溫故而知新」和「前端的知識雖然雜可是都有聯繫」。原本 Promise 的實現都被寫爛了,可是在學習函數式編程的時候竟然又繞回來了,這種感受實在奇妙,讓人不由佩服第一個產生 Promise 想法的人。Promise 將 函子(functor)觀察者模式 相結合,加以 狀態Promise 的解決過程 進行改造,最終得以實現一個異步解決方案。

篇幅有限,不免一些錯誤,歡迎探討和指教~
附一個 GitHub 完整的 repo 地址:github.com/LazyDuke/ts…

後記

這是一個系列,系列文章:

相關文章
相關標籤/搜索