深入理解 Promise 之手把手教你寫一版

什麼是 Promise?

爲什麼要用 Promise?

前端最令人頭疼的事情之一就是處理異步請求:

function load() {
    $.ajax({
        url: 'xxx.com',
        data: 'jsonp',
        success: function(res) {
            init(res, function(res) {
                render(res, function(res) {
                    // 千層餅
                });
            });
        }
    }
}

代碼層級多,可讀性差且難以維護,形成回調地獄。

有了 Promise,我們可以用同步操作的流程寫異步操作,解決了層層嵌套的回調函數的困擾:

new Promise(
  function (resolve, reject) {
    // 一段耗時的異步操作
    resolve('成功') 
    or
    reject('失敗') 
  }
 ).then(
   res => {console.log(res)},  // 成功
 ).catch(
   err => {console.log(err)}   // 失敗
 )

當然,Promise 也有缺點

事不宜遲,我們馬上開始!

Promise 的狀態

Promise 有以下 3 種狀態:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

狀態只能由 pending 向 fulfilled 或 rejected 轉變,且只有在執行環境堆棧僅包含平臺代碼時轉變一次,稱爲狀態凝固,並保存一個參數表明結果。

this.value = value   // fulfilled狀態,保存終值
this.reason = reason // rejected狀態,保存據因

Promise 構造函數

promise 構造函數接受一個函數作爲參數,我們稱該函數參數爲 executor,待 promise 執行時,會向 executor 傳入兩個函數參數,分別爲 resolve 和 reject,它們只做 3 件事:

其中第三條即爲 then 方法中配置的回調函數,這裏先不做多討論,先看前兩條,只需要兩行代碼即可:

this.state = state
this.value = value / this.reason = reason

我們先手擼一個簡單的構造函數:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise{
  constructor(executor) {
    this.state = PENDING  // prpmise具有初始狀態
    this.value = null// 用於保存終值
    this.reason = null// 用於保存拒因

    this.onFulfilledCallbacks = [] // 成功回調隊列
    this.onRejectedCallbacks = []  // 失敗回調隊列
    // 定義 resolve 函數
    // 這裏使用箭頭函數以解決 this 的指向,不瞭解的朋友可以先看阮大大的ES6文章
    const resolve = value => {
    // 保證狀態只能改變一次
        if (this.state === PENDING) {
          this.state = FULFILLED
          this.value = value
        }
    }
    // 同上
    const reject = reason => {
        if (this.state === PENDING) {
          this.state = REJECTED
          this.reason = reason
        }
    }
    // executor 可能會出現異常,需要捕獲並調用reject函數表示執行失敗
    try {
        // 傳入兩個函數參數
        executor(resolve, reject)
    } catch (e) {
        reject(e)
    }
  }
}

看上去還不錯,大概的流程已經完成了。還記得之前說過,狀態的改變是處於主線程空閒時,這裏使用 setTimeout 來模擬,以及 resolve/reject 還剩下第 3 件事,現在就讓我們一起完善它吧:

const resolve = value => {
     // setTimeout 模擬
     // 注意 即便是判斷狀態是否爲 pending 也應該要在主線程空閒時執行
     setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED
          this.value = value
          // 若是使用 forEach 回調函數有可能不按順序執行
          this.onFulfilledCallbacks.map(cb => cb(this.value))
        } 
     })
 }
 // reject同上

好啦,一個完整的構造函數就寫完了,是不是覺得很輕鬆,Promise 不過如此。

接下來是重頭戲 then 方法,then 接受兩個函數參數,分別爲 onFulfilled/onRejected,用來配置 promise 狀態改變後的回調函數。

其有兩個重點:

  1. 返回一個 promise2,以實現鏈式調用
  1. 監聽或執行對應的 onFulfilled/onRejected 回調函數

上述的 resolvePromise 我們先不理會,只要知道它是用來決定 promise2 的狀態即可。

首先,then 需要返回一個 promise2

then(onFulfilled, onRejected) {
        let promise2
        return (promise2 = new MyPromise((resolve, reject) => {
            
        })
    }

其次,then 方法的目的是配置或執行對應的 onFulfilled/onRejected 回調函數:

then(onFulfilled, onRejected) {
        let promise2
        return (promise2 = new MyPromise((resolve, reject) => {
            // 將回調函數配置好並推入對應的 callbacks 數組中
            this.onFulfilledCallbacks.push(value => {
                // 配置第一步:執行 callback 並保存返回值 x
                let x = onFulfilled(value);
                // 配置第二步:通過 resolvePromise 決定 promise2 狀態
                resolvePromise(promise2, x, resolve, reject)
            })
            // onRejected 同上
            this.onRejectedCallbacks.push(reason => {
                let x = onRejected(reason)
                resolvePromise(promise2, x, resolve, reject)
            })
        })
    }

在這裏可以大概瞭解 resolvePromise 是如何改變 promise2 狀態的,它接受 promise2 的 resolve/reject,由於箭頭函數的原因,resolve/reject 的 this 指向依舊指向 promise2,從而可以通過 resolvePromise 來改變狀態。

萬一 onFulfilled/onRejected 出錯怎麼辦?我們需要將它捕獲並將 promise2 的狀態改爲 rejected,我們將代碼再做修改:

then(onFulfilled, onRejected) {
        let promise2
        return (promise2 = new MyPromise((resolve, reject) => {
            // 將回調函數配置好並推入對應的 callbacks 數組中
            this.onFulfilledCallbacks.push(value => {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            
            })
            // onRejected 同上
            this.onRejectedCallbacks.push(reason => {
                try {
                    let x = onRejected(reason)
                    resolvePromise(promise2, x, resolve, reject) 
                } catch (e) {
                    reject(e)
                }
            })
        })
      }
   }

如果調用 then 方法的是已經狀態凝固的 promise 呢,也要推入 callbacks 數組嗎?答案當然不是,而是直接將配置好的 onFulfilled/onRejected 扔入 event-loop 中,就不勞煩 resolve/reject 了:

then(onFulfilled, onRejected){
    // fulfilled 狀態,將配置好的回調函數扔入 event-loop
    if (this.state === FULFILLED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }
    // rejected 狀態同上
    if (this.state === REJECTED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }
    // pending 狀態則交由 resolve/reject 來決定
    if (this.state === PENDING) {
      return (promise2 = new MyPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })

        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }
  }
}

看上去完美了,不過還差一件小事,假如 promise 使用者不按套路出牌,傳入的 onFulfilled/onRejected 不是一個函數怎麼辦?這裏我們就直接將之作爲返回值直接返回:

then(onFulfilled, onRejected){
    let promise2
    // 確保 onFulfilled/onRejected 爲函數
    // 若非函數,則轉換爲函數並且返回值爲自身
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
      throw reason
    }

    if (this.state === FULFILLED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }

    if (this.state === REJECTED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }

    if (this.state === PENDING) {
      return (promise2 = new MyPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })

        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }
  }
}

大功告成!

最後只剩下一個 resolvePromise 方法,先介紹一下它的功能:根據回調函數的返回值 x 決定 promise2 的最終狀態:

讓我們來一步一步刨析它吧:

function resolvePromise(promise2, x, resolve, reject){
    // 先從 x 爲 thenable 對象開始
    // 如果 x === promise2 需要拋出循環引用錯誤,否則會死循環
    if (x === promise2) {
        reject(newTypeError('循環引用'))
    }
    // 如果 x 就是 promise
    // 根據 x 的狀態來決定 promise2 的狀態
    if (x instanceof MyPromise) {
        // x 狀態爲 PENDING 時
        // 當 x 被 resolve 會調用新的 resolvePromise
        // 因爲怕 resolve 保存的終值還是 promise 繼續套娃
        // 所以一定要遞歸調用 resolvePromise 保證最終返回的一定是普通值
        // 失敗直接調用 reject 即可
        if (x.state === PENDING) {
            x.then(
                y => {
                    resolvePromise(promise2, y, resolve, reject)
                },
                r => {
                    reject(r)
                }
            )
        } else {
            // x 狀態凝固,直接配置即可
            // 不過這裏有個疑問
            // 如果之前 resolve 保存的終值還是 promise 呢
            // 該怎樣預防這一問題,後續將會講到
            x.then(resolve, reject)
        }
    }
}

現在把應對 x 的值爲 promise 的代碼書寫完畢,但這還不夠,我們要面對的不只是 promise,而是一個 thenable 對象,所以還要繼續判斷:

function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(newTypeError('循環引用'))
  }
  if (x instanceof MyPromise) {
     // 前面的代碼不再贅述
  } elseif (x && (typeof x === 'function' || typeof x === 'object')) {
      // 因爲不一定是規範的 promise 對象
      // 我們需要保證狀態的改變只發生一次
      // 加入一個 called 變量來加鎖
      let called = false
      // 還是因爲不一定是規範的 promise 對象
      // 需要保證運行時異常能夠被捕獲
      try {
          // 注意,前面不加 try/catch
          // 僅僅下面這一行代碼也有可能會報錯而無法被捕獲
          let then = x.then
          // 假如 x.then 存在併爲函數
          if (typeof then === 'function') {
              // 使用 call 方法保證 then 的調用對象爲 x
              then.call{
                  x,
                  y => {
                      // 假如狀態凝固便不再執行
                      if (called) return
                      called = true
                      // 防止出現 resolve 保存 promise 的情況
                      resolvePromise(promise2, y, resolve, reject)
                  },
                  r => {
                      // 同上
                      if (called) return
                      called = true
                      reject(r)
                  }
              } 
         } else {
            // 如果 x.then 不是函數
            // 即爲普通值,直接 resolve 就好
            resolve(x)
        }
       } catch (e) {
          // 若調用一個不正規的 thenalbe 對象出錯
          // 拋出異常
          // 這裏要注意,這裏出現錯誤很有可能是執行了 x.then 方法,而之前也說過,其不一定正規,可能狀態已經凝固,需要多加一重保險
          if (called) return
          called = true
          reject(e)
        }
     } else {
       // 不是 thenable 對象,那就是普通值
       // 直接 resolve
       resolve(x)   
    }
}

一套行雲流水的代碼寫下來,我們的 promise 就完成了,不過還記得之前代碼裏留了個疑問嗎?當 x 爲 promise 且狀態凝固時,如果確定它保存的終值的不是 promise 呢?其實只要最開始的 resolve 函數多加一重判斷即可:

const resolve = value => {
      if (value instanceof MyPromise) {
        return value.then(resolve, reject)
      }

      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED
          this.value = value
          this.onFulfilledCallbacks.map(cb => cb(this.value))
        }
      })
    }

再次防止套娃!

好啦,也許你會問,我怎麼知道這個手寫的 promise 就一定是正確的呢?接下來將一步步帶你驗證!

首先找到一個空文件夾,在命令行輸入:

    npm init -y
    // 下載 promise 測試工具
    npm install promises-aplus-tests -D

新建 promise.js 文件,並將你實現的 promise 複製於此,並在下方加入一下代碼:

MyPromise.deferred = function() {
  let defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  });
  return defer
}

module.exports = MyPromise

再修改 package.json 文件如下:

{
  "name": "promise",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "promises-aplus-tests ./promise.js"
  },
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  }
}

最後一步:

    npm run test

完成!

手寫 Promise,你也可以!

最後附上完整實現代碼

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  constructor(executor) {
    this.state = PENDING
    this.value = null
    this.reason = null

    this.onFulfilledCallbacks = []
    this.onRejectedCallbacks = []

    const resolve = value => {
      if (value instanceof MyPromise) {
        return value.then(resolve, reject)
      }

      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = FULFILLED
          this.value = value
          this.onFulfilledCallbacks.map(cb => cb(this.value))
        }
      })
    }

    const reject = reason => {
      setTimeout(() => {
        if (this.state === PENDING) {
          this.state = REJECTED
          this.reason = reason
          this.onRejectedCallbacks.map(cb => cb(this.reason))
        }
      })
    }

    try {
      executor(resolve, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    let promise2

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {
      throw reason
    }

    if (this.state === FULFILLED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }

    if (this.state === REJECTED) {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }

    if (this.state === PENDING) {
      return (promise2 = new MyPromise((resolve, reject) => {
        this.onFulfilledCallbacks.push(value => {
          try {
            let x = onFulfilled(value)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })

        this.onRejectedCallbacks.push(reason => {
          try {
            let x = onRejected(reason)
            resolvePromise(promise2, x, resolve, reject)
          } catch (e) {
            reject(e)
          }
        })
      }))
    }
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  if (x === promise2) {
    reject(newTypeError('循環引用'))
  }

  if (x instanceof MyPromise) {
    if (x.state === PENDING) {
      x.then(
        y => {
          resolvePromise(promise2, y, resolve, reject)
        },
        r => {
          reject(r)
        }
      )
    } else {
      x.then(resolve, reject)
    }
  } elseif (x && (typeof x === 'function' || typeof x === 'object')) {
    let called = false
    try {
      let then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          y => {
            if (called) return
            called = true
            resolvePromise(promise2, y, resolve, reject)
          },
          r => {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        resolve(x)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(x)
  }
}

MyPromise.deferred = function() {
  let defer = {}
  defer.promise = new MyPromise((resolve, reject) => {
    defer.resolve = resolve
    defer.reject = reject
  });
  return defer
};

module.exports = MyPromise;
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/_gDIO6YCswAS2dICllMG0A