如何實現(xiàn)一個promise

image

promise 是 ES6 中新增的一種異步解決方案,在日常開發(fā)中也經(jīng)常能看見它的身影汽煮,例如原生的 fetch API 就是基于 promise 實現(xiàn)的棚唆。那么 promise 有哪些特性,如何實現(xiàn)一個具有 promise/A+ 規(guī)范的 promise 呢翎卓?

promise 特性

首先我們整理一下 promise 的一些基本特性和 API摆寄,完整的 promise/A+ 規(guī)范可以參考 【翻譯】Promises/A+規(guī)范

  • 狀態(tài)機(jī)
    • 具有 pending微饥、fulfilled、rejected 三個狀態(tài)
    • 只能由 pending -> fulfilled 和 pending -> rejected 這兩種狀態(tài)變化矩肩,且一經(jīng)改變之后狀態(tài)不可再變
    • 成功時必須有一個不可改變的值 value肃续,失敗時必須有一個不可改變的拒因 reason
  • 構(gòu)造函數(shù)
    • Promise 接受一個函數(shù)作為參數(shù),函數(shù)擁有兩個參數(shù) fulfill 和 reject
    • fulfill 將 promise 狀態(tài)從 pending 置為 fulfilled刽酱,返回操作的結(jié)果
    • reject 將 promise 狀態(tài)從 pending 置為 rejected棵里,返回產(chǎn)生的錯誤
  • then 方法
    • 接受兩個參數(shù) onFulfilled 和 onRejected,分別表示 promise 成功和失敗的回調(diào)
    • 返回值會作為參數(shù)傳遞到下一個 then 方法的參數(shù)中
  • 異步處理
  • 鏈?zhǔn)秸{(diào)用
  • 其他 API
    • catch典蝌、finally
    • resolve头谜、reject乔夯、race、all 等

實現(xiàn)

接下來我們逐步實現(xiàn)一個具有 promise/A+ 規(guī)范的 promise

基本實現(xiàn)

先定義一個常量侧纯,表示 promise 的三個狀態(tài)

const STATE = {
  PENDING: 'pending',
  FULFILLED: 'fulfilled',
  REJECTED: 'rejected'
}

然后在 promise 中初始化兩個參數(shù) value 和 reason貌嫡,分別表示狀態(tài)為 fulfill 和 reject 時的值,接著定義兩個函數(shù)馋辈,函數(shù)內(nèi)部更新狀態(tài)以及相應(yīng)的字段值墩新,分別在成功和失敗的時候執(zhí)行,然后將這兩個函數(shù)傳入構(gòu)造函數(shù)的函數(shù)參數(shù)中绵疲,如下:

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

    // 成功
    const fulfill = (value) => {
      // 只有 state 為 pending 時盔憨,才可以更改狀態(tài)
      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
      }
    }
    // 執(zhí)行函數(shù)出錯時調(diào)用 reject
    try {
      fn(fulfill, reject)
    } catch (e) {
      reject(e)
    }
  }
}

接下來初步實現(xiàn)一個 then 方法郁岩,當(dāng)當(dāng)前狀態(tài)是 fulfulled 時,執(zhí)行成功回調(diào)萍摊,當(dāng)前狀態(tài)為 rejected 時蝴乔,執(zhí)行失敗回調(diào):

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

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

這個時候一個簡單的 MyPromise 就實現(xiàn)了薇正,但是此時它還只能處理同步任務(wù)囚衔,對于異步操作卻無能為力

異步處理

要想處理異步操作练湿,可以利用隊列的特性,將回調(diào)函數(shù)先緩存起來辽俗,等到異步操作的結(jié)果返回之后篡诽,再去執(zhí)行相應(yīng)的回調(diào)函數(shù)杈女。

具體實現(xiàn)來看,在 then 方法中增加判斷翰蠢,若為 pending 狀態(tài)啰劲,將傳入的函數(shù)寫入對應(yīng)的回調(diào)函數(shù)隊列蝇裤;在初始化 promise 時利用兩個數(shù)組分別保存成功和失敗的回調(diào)函數(shù)隊列,并在 fulfill 和 reject 回調(diào)中增加它們酥泞。如下:

class MyPromise {
  constructor(fn) {
    // 初始化
    this.state = STATE.PENDING
    this.value = null
    this.reason = null
    // 保存數(shù)組
    this.fulfilledCallbacks = []
    this.rejectedCallbacks = []
    // 成功
    const fulfill = (value) => {
      // 只有 state 為 pending 時芝囤,才可以更改狀態(tài)
      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())
      }
    }
    // 執(zhí)行函數(shù)出錯時調(diào)用 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)
    }
    // 當(dāng) then 是 pending 時,將這兩個狀態(tài)寫入數(shù)組中
    if (this.state === STATE.PENDING) {
      this.fulfilledCallbacks.push(() => {
        onFulfilled(this.value)
      })
      this.rejectedCallbacks.push(() => {
        onRejected(this.reason)
      })
    }
  }
}

鏈?zhǔn)秸{(diào)用

接下來對 MyPromise 進(jìn)行進(jìn)一步改造羡藐,使其能夠支持鏈?zhǔn)秸{(diào)用仆嗦,使用過 jquery 等庫應(yīng)該對于鏈?zhǔn)秸{(diào)用非常熟悉先壕,它的原理就是調(diào)用者返回它本身垃僚,在這里的話就是要讓 then 方法返回一個 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))
      }
      // 當(dāng) then 是 pending 時栽燕,將這兩個狀態(tài)寫入數(shù)組中
      if (this.state === STATE.PENDING) {
        this.fulfilledCallbacks.push(() => {
          fulfill(onFulfilled(this.value))
        })
        this.rejectedCallbacks.push(() => {
          reject(onRejected(this.reason))
        })
      }
    })
  }
}

實現(xiàn)到這一步的 MyPromise 已經(jīng)可以支持異步操作碍岔、鏈?zhǔn)秸{(diào)用朵夏、傳遞返回值侍郭,算是一個簡易版的 promise,一般來說面試時需要手寫一個 promise 時猛计,到這個程度就足夠了爆捞,完整實現(xiàn) promise/A+ 規(guī)范在面試這樣一個較短的時間內(nèi)也不太現(xiàn)實煮甥。

到這一步的完整代碼可以參考 promise3.js

promise/A+ 規(guī)范

promise/A+ 規(guī)范中規(guī)定,onFulfilled/onRejected 返回一個值 x卖局,對 x 需要作以下處理:

  • 如果 x 與 then 方法返回的 promise 相等砚偶,拋出一個 TypeError 錯誤
  • 如果 x 是一個 Promise ,則保持 then 方法返回的 promise 的值與 x 的值一致
  • 如果 x 是對象或函數(shù)均芽,則將 x.then 賦值給 then 并調(diào)用
    • 如果 then 是一個函數(shù)单鹿,則將 x 作為作用域 this 調(diào)用仲锄,并傳遞兩個參數(shù) resolvePromiserejectPromise,如果 resolvePromiserejectPromise 均被調(diào)用或者被調(diào)用多次是趴,則采用首次調(diào)用并忽略剩余調(diào)用
    • 如果調(diào)用 then 方法出錯,則以拋出的錯誤 e 為拒因拒絕 promise
    • 如果 then 不是函數(shù)富雅,則以 x 為參數(shù)執(zhí)行 promise
  • 如果 x 是其他值没佑,則以 x 為參數(shù)執(zhí)行 promise

接下來對上一步實現(xiàn)的 MyPromise 進(jìn)行進(jìn)一步優(yōu)化,使其符合 promise/A+ 規(guī)范:

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)
        }
      }
      // 當(dāng) then 是 pending 時,將這兩個狀態(tài)寫入數(shù)組中
      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 的行為封裝成為了一個函數(shù) generatePromise待秃,實現(xiàn)如下:

const generatePromise = (promise2, x, fulfill, reject) => {
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'))
  }
  // 如果 x 是 promise痹屹,調(diào)用它的 then 方法繼續(xù)遍歷
  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')) {
    // 防止重復(fù)調(diào)用志衍,成功和失敗只能調(diào)用一次
    let called;
    // 如果 x 是對象或函數(shù)
    try {
      const then = x.then
      if (typeof then === 'function') {
        then.call(x, (y) => {
          if (called) return;
          called = true;
          // 說明 y是 promise楼肪,繼續(xù)遍歷
          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+ 規(guī)范中還規(guī)定,對于 promise2 = promise1.then(onFulfilled, onRejected)

  • onFulfilled/onRejected 必須異步調(diào)用肩钠,不能同步
  • 如果 onFulfilled 不是函數(shù)且 promise1 成功執(zhí)行蔬将, promise2 必須成功執(zhí)行并返回相同的值
  • 如果 onRejected 不是函數(shù)且 promise1 拒絕執(zhí)行, promise2 必須拒絕執(zhí)行并返回相同的拒因

對于 then 方法做最后的完善惫东,增加 setTimeout 模擬異步調(diào)用毙石,增加對于 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 宏任務(wù)徐矩,確保onFulfilled 和 onRejected 異步執(zhí)行
      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)
      }
      // 當(dāng) then 是 pending 時滤灯,將這兩個狀態(tài)寫入數(shù)組中
      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
  }
}

實現(xiàn) promise/A+ 規(guī)范的 promise 完整代碼可以參考 promise4.js

如何知道你實現(xiàn)的 promise 是否遵循 promise/A+ 規(guī)范呢鳞骤?可以利用 promises-aplus-tests 這樣一個 npm 包來進(jìn)行相應(yīng)測試

其他 API

這里對其他常用的 promise API 進(jìn)行了實現(xiàn)

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 狀態(tài)的 Promise 對象

MyPromise.resolve = (value) => {
  // 傳入 promise 類型直接返回
  if (value instanceof MyPromise) return value
  // 傳入 thenable 對象時,立即執(zhí)行 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 狀態(tài)的 Promise 對象

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

Promise.race

返回一個 promise渤滞,一旦迭代器中的某個 promise 狀態(tài)改變榴嗅,返回的 promise 狀態(tài)隨之改變

MyPromise.race = (promises) => {
  return new MyPromise((resolve, reject) => {
    // promises 可以不是數(shù)組录肯,但必須存在 Iterator 接口,因此采用 for...of 遍歷
    for(let promise of promises) {
      // 如果當(dāng)前值不是 Promise优炬,通過 resolve 方法轉(zhuǎn)為 promise
      if (promise instanceof MyPromise) {
        promise.then(resolve, reject)
      } else {
        MyPromise.resolve(promise).then(resolve, reject)
      }
    }
  })
}

Promise.all

返回一個 promise蠢护,只有迭代器中的所有的 promise 均變?yōu)?fulfilled养涮,返回的 promise 才變?yōu)?fulfilled眉抬,迭代器中出現(xiàn)一個 rejected蜀变,返回的 promise 變?yōu)?rejected

MyPromise.all = (promises) => {
  return new MyPromise((resolve, reject) => {
    const arr = []
    // 已返回數(shù)
    let count = 0
    // 當(dāng)前索引
    let index = 0
    // promises 可以不是數(shù)組介评,但必須存在 Iterator 接口们陆,因此采用 for...of 遍歷
    for(let promise of promises) {
      // 如果當(dāng)前值不是 Promise,通過 resolve 方法轉(zhuǎn)為 promise
      if (!(promise instanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // 使用閉包保證異步返回數(shù)組順序
      ((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 狀態(tài)的 promise喂很,并且返回的 promise 狀態(tài)總是 fulfilled雾袱,不會返回 rejected 狀態(tài)

MyPromise.allSettled = (promises) => {
  return new MyPromise((resolve, reject) => {
    const arr = []
    // 已返回數(shù)
    let count = 0
    // 當(dāng)前索引
    let index = 0
    // promises 可以不是數(shù)組芹橡,但必須存在 Iterator 接口望伦,因此采用 for...of 遍歷
    for(let promise of promises) {
      // 如果當(dāng)前值不是 Promise屯伞,通過 resolve 方法轉(zhuǎn)為 promise
      if (!(promise instanceof MyPromise)) {
        promise = MyPromise.resolve(promise)
      }
      // 使用閉包保證異步返回數(shù)組順序
      ((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
    }
  })
}

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

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末珠移,一起剝皮案震驚了整個濱河市末融,隨后出現(xiàn)的幾起案子勾习,更是在濱河造成了極大的恐慌,老刑警劉巖巧婶,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異湾盒,居然都是意外死亡罚勾,警方通過查閱死者的電腦和手機(jī)漾唉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門赵刑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蚪战,你說我怎么就攤上這事邀桑】坪酰” “怎么了?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長空闲。 經(jīng)常有香客問我碴倾,道長,這世上最難降的妖魔是什么异雁? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任矫户,我火速辦了婚禮,結(jié)果婚禮上柑蛇,老公的妹妹穿的比我還像新娘耻台。我一直安慰自己,他們只是感情好盆耽,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布坝咐。 她就那樣靜靜地躺著析恢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪泽篮。 梳的紋絲不亂的頭發(fā)上柑船,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天鞍时,我揣著相機(jī)與錄音逆巍,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛溪烤,可吹牛的內(nèi)容都是我干的庇勃。 我是一名探鬼主播责嚷,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼罕拂,長吁一口氣:“原來是場噩夢啊……” “哼衷掷!你這毒婦竟也來了雨涛?” 一聲冷哼從身側(cè)響起懦胞,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤躏尉,失蹤者是張志新(化名)和其女友劉穎醇份,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矩距,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡锥债,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年哮肚,在試婚紗的時候發(fā)現(xiàn)自己被綠了广匙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸦致。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡分唾,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出弧蝇,到底是詐尸還是另有隱情,我是刑警寧澤沙峻,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布专酗,位于F島的核電站盗扇,受9級特大地震影響疗隶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蒋纬,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一坚弱、第九天 我趴在偏房一處隱蔽的房頂上張望荒叶。 院中可真熱鬧,春花似錦脂凶、人聲如沸愁茁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春泞当,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背民珍。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工襟士, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留盗飒,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓陋桂,卻偏偏與公主長得像逆趣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嗜历,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容