如何實現一個promise

promise 是 ES6 中新增的一種異步解決方案,在平常開發中也常常能看見它的身影,例如原生的 fetch API 就是基於 promise 實現的。那麼 promise 有哪些特性,如何實現一個具備 promise/A+ 規範的 promise 呢?javascript

promise 特性

首先咱們整理一下 promise 的一些基本特性和 API,完整的 promise/A+ 規範能夠參考 【翻譯】Promises/A+規範java

  • 狀態機
    • 具備 pending、fulfilled、rejected 三個狀態
    • 只能由 pending -> fulfilled 和 pending -> rejected 這兩種狀態變化,且一經改變以後狀態不可再變
    • 成功時必須有一個不可改變的值 value,失敗時必須有一個不可改變的拒因 reason
  • 構造函數
    • Promise 接受一個函數做爲參數,函數擁有兩個參數 fulfill 和 reject
    • fulfill 將 promise 狀態從 pending 置爲 fulfilled,返回操做的結果
    • reject 將 promise 狀態從 pending 置爲 rejected,返回產生的錯誤
  • then 方法
    • 接受兩個參數 onFulfilled 和 onRejected,分別表示 promise 成功和失敗的回調
    • 返回值會做爲參數傳遞到下一個 then 方法的參數中
  • 異步處理
  • 鏈式調用
  • 其餘 API
    • catch、finally
    • resolve、reject、race、all 等

實現

接下來咱們逐步實現一個具備 promise/A+ 規範的 promisejquery

基本實現

先定義一個常量,表示 promise 的三個狀態git

const STATE = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}
複製代碼

而後在 promise 中初始化兩個參數 value 和 reason,分別表示狀態爲 fulfill 和 reject 時的值,接着定義兩個函數,函數內部更新狀態以及相應的字段值,分別在成功和失敗的時候執行,而後將這兩個函數傳入構造函數的函數參數中,以下:es6

class MyPromise {
  constructor(fn) {
    // 初始化
    this.state = STATE.PENDING
    this.value = null
    this.reason = null

    // 成功
    const fulfill = (value) => {
      // 只有 state 爲 pending 時,才能夠更改狀態
      if (this.state === STATE.PENDING) {
        this.state = STATE.FULFILLED
        this.value = value
      }
    }

    // 失敗
    const reject = (reason) => {
      if (this.state === STATE.PENDING) {
        this.state = STATE.REJECTED
        this.reason = reason
      }
    }
    // 執行函數出錯時調用 reject
    try {
      fn(fulfill, reject)
    } catch (e) {
      reject(e)
    }
  }
}
複製代碼

接下來初步實現一個 then 方法,噹噹前狀態是 fulfulled 時,執行成功回調,當前狀態爲 rejected 時,執行失敗回調:github

class MyPromise {
  constructor(fn) {
    //...
  }

  then(onFulfilled, onRejected) {
    if (this.state === STATE.FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === STATE.REJECTED) {
      onRejected(this.reason)
    }
  }
}

複製代碼

這個時候一個簡單的 MyPromise 就實現了,可是此時它還只能處理同步任務,對於異步操做卻無能爲力面試

異步處理

要想處理異步操做,能夠利用隊列的特性,將回調函數先緩存起來,等到異步操做的結果返回以後,再去執行相應的回調函數。npm

具體實現來看,在 then 方法中增長判斷,若爲 pending 狀態,將傳入的函數寫入對應的回調函數隊列;在初始化 promise 時利用兩個數組分別保存成功和失敗的回調函數隊列,並在 fulfill 和 reject 回調中增長它們。以下:數組

class MyPromise {
  constructor(fn) {
    // 初始化
    this.state = STATE.PENDING
    this.value = null
    this.reason = null
    // 保存數組
    this.fulfilledCallbacks = []
    this.rejectedCallbacks = []
    // 成功
    const fulfill = (value) => {
      // 只有 state 爲 pending 時,才能夠更改狀態
      if (this.state === STATE.PENDING) {
        this.state = STATE.FULFILLED
        this.value = value
        this.fulfilledCallbacks.forEach(cb => cb())
      }
    }

    // 失敗
    const reject = (reason) => {
      if (this.state === STATE.PENDING) {
        this.state = STATE.REJECTED
        this.reason = reason
        this.rejectedCallbacks.forEach(cb => cb())
      }
    }
    // 執行函數出錯時調用 reject
    try {
      fn(fulfill, reject)
    } catch (e) {
      reject(e)
    }
  }

  then(onFulfilled, onRejected) {
    if (this.state === STATE.FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.state === STATE.REJECTED) {
      onRejected(this.reason)
    }
    // 當 then 是 pending 時,將這兩個狀態寫入數組中
    if (this.state === STATE.PENDING) {
      this.fulfilledCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.rejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}
複製代碼

鏈式調用

接下來對 MyPromise 進行進一步改造,使其可以支持鏈式調用,使用過 jquery 等庫應該對於鏈式調用很是熟悉,它的原理就是調用者返回它自己,在這裏的話就是要讓 then 方法返回一個 promise 便可,還有一點就是對於返回值的傳遞:promise

class MyPromise {
  constructor(fn) {
    //...
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((fulfill, reject) => {
      if (this.state === STATE.FULFILLED) {
        // 將返回值傳入下一個 fulfill 中
        fulfill(onFulfilled(this.value))
      }
      if (this.state === STATE.REJECTED) {
        // 將返回值傳入下一個 reject 中
        reject(onRejected(this.reason))
      }
      // 當 then 是 pending 時,將這兩個狀態寫入數組中
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push(() => {
          fulfill(onFulfilled(this.value))
        })
        this.rejectedCallbacks.push(() => {
          reject(onRejected(this.reason))
        })
      }
    })
  }
}
複製代碼

實現到這一步的 MyPromise 已經能夠支持異步操做、鏈式調用、傳遞返回值,算是一個簡易版的 promise,通常來講面試時須要手寫一個 promise 時,到這個程度就足夠了,完整實現 promise/A+ 規範在面試這樣一個較短的時間內也不太現實。

到這一步的完整代碼能夠參考 promise3.js

promise/A+ 規範

promise/A+ 規範中規定,onFulfilled/onRejected 返回一個值 x,對 x 須要做如下處理:

  • 若是 x 與 then 方法返回的 promise 相等,拋出一個 TypeError 錯誤
  • 若是 x 是一個 Promise ,則保持 then 方法返回的 promise 的值與 x 的值一致
  • 若是 x 是對象或函數,則將 x.then 賦值給 then 並調用
    • 若是 then 是一個函數,則將 x 做爲做用域 this 調用,並傳遞兩個參數 resolvePromiserejectPromise,若是 resolvePromiserejectPromise 均被調用或者被調用屢次,則採用首次調用並忽略剩餘調用
    • 若是調用 then 方法出錯,則以拋出的錯誤 e 爲拒因拒絕 promise
    • 若是 then 不是函數,則以 x 爲參數執行 promise
  • 若是 x 是其餘值,則以 x 爲參數執行 promise

接下來對上一步實現的 MyPromise 進行進一步優化,使其符合 promise/A+ 規範:

class MyPromise {
  constructor(fn) {
    //...
  }

  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((fulfill, reject) => {
      if (this.state === STATE.FULFILLED) {
        try {
          const x = onFulfilled(this.value)
          generatePromise(promise2, x, fulfill, reject)
        } catch (e) {
          reject(e)
        }
      }
      if (this.state === STATE.REJECTED) {
        try {
          const x = onRejected(this.reason)
          generatePromise(promise2, x, fulfill, reject)
        } catch (e) {
          reject(e)
        }
      }
      // 當 then 是 pending 時,將這兩個狀態寫入數組中
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push(() => {
          try {
            const x = onFulfilled(this.value)
            generatePromise(promise2, x, fulfill, reject)
          } catch(e) {
            reject(e)
          }
        })
        this.rejectedCallbacks.push(() => {
          try {
            const x = onRejected(this.reason)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        })
      }
    })
    return promise2
  }
}
複製代碼

這裏將處理返回值 x 的行爲封裝成爲了一個函數 generatePromise,實現以下:

const generatePromise = (promise2, x, fulfill, reject) => {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }
  // 若是 x 是 promise,調用它的 then 方法繼續遍歷
  if (x instanceof MyPromise) {
    x.then((value) => {
      generatePromise(promise2, value, fulfill, reject)
    }, (e) => {
      reject(e)
    })
  } else if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    // 防止重複調用,成功和失敗只能調用一次
    let called;
    // 若是 x 是對象或函數
    try {
      const then = x.then
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (called) return;
          called = true;
          // 說明 y是 promise,繼續遍歷
          generatePromise(promise2, y, fulfill, reject)
        }, (r) => {
          if (called) return;
          called = true;
          reject(r)
        })
      } else {
        fulfill(x)
      }
    } catch(e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    fulfill(x)
  }
}
複製代碼

promise/A+ 規範中還規定,對於 promise2 = promise1.then(onFulfilled, onRejected)

  • onFulfilled/onRejected 必須異步調用,不能同步
  • 若是 onFulfilled 不是函數且 promise1 成功執行, promise2 必須成功執行並返回相同的值
  • 若是 onRejected 不是函數且 promise1 拒絕執行, promise2 必須拒絕執行並返回相同的拒因

對於 then 方法作最後的完善,增長 setTimeout 模擬異步調用,增長對於 onFulfilled 和 onRejected 方法的判斷:

class MyPromise {
  constructor(fn) {
    //...
  }

  then(onFulfilled, onRejected) {
    // 處理 onFulfilled 和 onRejected
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e }
    const promise2 = new MyPromise((fulfill, reject) => {
      // setTimeout 宏任務,確保onFulfilled 和 onRejected 異步執行
      if (this.state === STATE.FULFILLED) {
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      if (this.state === STATE.REJECTED) {
        setTimeout(() => {
          try {
            const x = onRejected(this.reason)
            generatePromise(promise2, x, fulfill, reject)
          } catch (e) {
            reject(e)
          }
        }, 0)
      }
      // 當 then 是 pending 時,將這兩個狀態寫入數組中
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value)
              generatePromise(promise2, x, fulfill, reject)
            } catch(e) {
              reject(e)
            }
          }, 0)
        })
        this.rejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason)
              generatePromise(promise2, x, fulfill, reject)
            } catch (e) {
              reject(e)
            }
          }, 0)
        })
      }
    })
    return promise2
  }
}
複製代碼

實現 promise/A+ 規範的 promise 完整代碼能夠參考 promise4.js

如何知道你實現的 promise 是否遵循 promise/A+ 規範呢?能夠利用 promises-aplus-tests 這樣一個 npm 包來進行相應測試

其餘 API

這裏對其餘經常使用的 promise API 進行了實現

catch、finally

class MyPromise {
  constructor(fn) {
    //...
  }
  then(onFulfilled, onRejected) {
    //...
  }
  catch(onRejected) {
    return this.then(null, onRejected)
  }
  finally(callback) {
    return this.then(callback, callback)
  }
}
複製代碼

Promise.resolve

返回一個 resolved 狀態的 Promise 對象

MyPromise.resolve = (value) => {
  // 傳入 promise 類型直接返回
  if (value instanceof MyPromise) return value
  // 傳入 thenable 對象時,當即執行 then 方法
  if (value !== null && typeof value === 'object') {
    const then = value.then
    if (then && typeof then === 'function') return new MyPromise(value.then)
  }
  return new MyPromise((resolve) => {
    resolve(value)
  })
}
複製代碼

Promise.reject

返回一個 rejected 狀態的 Promise 對象

MyPromise.reject = (reason) => {
  // 傳入 promise 類型直接返回
  if (reason instanceof MyPromise) return reason
  return new MyPromise((resolve, reject) => {
    reject(reason)
  })
}
複製代碼

Promise.race

返回一個 promise,一旦迭代器中的某個 promise 狀態改變,返回的 promise 狀態隨之改變

MyPromise.race = (promises) => {
  return new MyPromise((resolve, reject) => {
    // promises 能夠不是數組,但必須存在 Iterator 接口,所以採用 for...of 遍歷
    for(let promise of promises) {
      // 若是當前值不是 Promise,經過 resolve 方法轉爲 promise
      if (promise instanceof MyPromise) {
        promise.then(resolve, reject)
      } else {
        MyPromise.resolve(promise).then(resolve, reject)
      }
    }
  })
}
複製代碼

Promise.all

返回一個 promise,只有迭代器中的全部的 promise 均變爲 fulfilled,返回的 promise 才變爲 fulfilled,迭代器中出現一個 rejected,返回的 promise 變爲 rejected

MyPromise.all = (promises) => {
  return new MyPromise((resolve, reject) => {
    const arr = []
    // 已返回數
    let count = 0
    // 當前索引
    let index = 0
    // promises 能夠不是數組,但必須存在 Iterator 接口,所以採用 for...of 遍歷
    for(let promise of promises) {
      // 若是當前值不是 Promise,經過 resolve 方法轉爲 promise
      if (!(promise instanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // 使用閉包保證異步返回數組順序
      ((i) => {
        promise.then((value) => {
          arr[i] = value
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        }, reject)
      })(index)
      // index 遞增
      index += 1
    }
  })
}
複製代碼

Promise.allSettled

只有等到迭代器中全部的 promise 都返回,纔會返回一個 fulfilled 狀態的 promise,而且返回的 promise 狀態老是 fulfilled,不會返回 rejected 狀態

MyPromise.allSettled = (promises) => {
  return new MyPromise((resolve, reject) => {
    const arr = []
    // 已返回數
    let count = 0
    // 當前索引
    let index = 0
    // promises 能夠不是數組,但必須存在 Iterator 接口,所以採用 for...of 遍歷
    for(let promise of promises) {
      // 若是當前值不是 Promise,經過 resolve 方法轉爲 promise
      if (!(promise instanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // 使用閉包保證異步返回數組順序
      ((i) => {
        promise.then((value) => {
          arr[i] = value
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        }, (err) => {
          arr[i] = err
          count += 1
          if (count === promises.length || count === promises.size) {
            resolve(arr)
          }
        })
      })(index)
      // index 遞增
      index += 1
    }
  })
}
複製代碼

本文若有錯誤,歡迎批評指正~

參考

相關文章
相關標籤/搜索