30分鐘,帶你實(shí)現(xiàn)一個(gè)符合規(guī)范的 Promise(巨詳細(xì))

img2.jpeg

前言

關(guān)于 Promise 原理解析的優(yōu)秀文章,在掘金上已經(jīng)有非常多了。但是筆者總是處在 看了就會(huì),一寫就廢 的狀態(tài),這是筆者寫這篇文章的目的,為了理一下 Promise 的編寫思路,從零開始手寫一波代碼,同時(shí)也方便自己日后回顧。

?

Promise 的作用

PromiseJavaScript 異步編程的一種流行解決方案,它的出現(xiàn)是為了解決 回調(diào)地獄 的問題,讓使用者可以通過鏈?zhǔn)降膶懛ㄈゾ帉憣懏惒酱a,具體的用法筆者就不介紹了,大家可以參考阮一峰老師的 ES6 Promise教程

?

課前知識(shí)

觀察者模式

什么是觀察者模式:

觀察者模式定義了一種一對(duì)多的依賴關(guān)系,讓多個(gè)觀察者對(duì)象同時(shí)監(jiān)聽某一個(gè)目標(biāo)對(duì)象,當(dāng)這個(gè)目標(biāo)對(duì)象的狀態(tài)發(fā)生變化時(shí),會(huì)通知所有觀察者對(duì)象,使它們能夠自動(dòng)更新。

Promise 是基于 觀察者的設(shè)計(jì)模式 實(shí)現(xiàn)的,then 函數(shù)要執(zhí)行的函數(shù)會(huì)被塞入觀察者數(shù)組中,當(dāng) Promise 狀態(tài)變化的時(shí)候,就去執(zhí)行觀察組數(shù)組中的所有函數(shù)。

事件循環(huán)機(jī)制

實(shí)現(xiàn) Promise 涉及到了 JavaScript 中的事件循環(huán)機(jī)制 EventLoop、以及宏任務(wù)和微任務(wù)的概念。

事件循環(huán)機(jī)制的流程圖如下:

image

大家可以看一下這段代碼:

console.log(1);

setTimeout(() => {
  console.log(2);
},0);

let a = new Promise((resolve) => {
  console.log(3);
  resolve();
}).then(() => {
  console.log(4);
}).then(() => {
  console.log(5);
});

console.log(6);

如果不能一下子說出輸出結(jié)果,建議大家可以先查閱一下 事件循環(huán) 的相關(guān)資料,在掘金中有很多優(yōu)秀的文章。

Promises/A+ 規(guī)范

Promises/A+ 是一個(gè)社區(qū)規(guī)范,如果你想寫出一個(gè)規(guī)范的 Promise,我們就需要遵循這個(gè)標(biāo)準(zhǔn)。之后我們也會(huì)根據(jù)規(guī)范來完善我們自己編寫的 Promise。

?

Promise 核心知識(shí)點(diǎn)

在動(dòng)手寫 Promise 之前,我們先過一下幾個(gè)重要的知識(shí)點(diǎn)。

executor

// 創(chuàng)建 Promise 對(duì)象 x1
// 并在 executor 函數(shù)中執(zhí)行業(yè)務(wù)邏輯
function executor(resolve, reject){
  // 業(yè)務(wù)邏輯處理成功結(jié)果
  const value = ...;
  resolve(value);
  // 失敗結(jié)果
  // const reason = ...;
  // reject(reason);
}

let x1 = new Promise(executor);

首先 Promise 是一個(gè)類,它接收一個(gè)執(zhí)行函數(shù) executor,它接收兩個(gè)參數(shù):resolvereject,這兩個(gè)參數(shù)是 Promise 內(nèi)部定義的兩個(gè)函數(shù),用來改變狀態(tài)并執(zhí)行對(duì)應(yīng)回調(diào)函數(shù)。

因?yàn)?Promise 本身是不知道執(zhí)行結(jié)果失敗或者成功,它只是給異步操作提供了一個(gè)容器,實(shí)際上的控制權(quán)在使用者的手上,使用者可以調(diào)用上面兩個(gè)參數(shù)告訴 Promise 結(jié)果是否成功,同時(shí)將業(yè)務(wù)邏輯處理結(jié)果(value/reason)作為參數(shù)傳給 resolvereject 兩個(gè)函數(shù),執(zhí)行回調(diào)。

三個(gè)狀態(tài)

Promise 有三個(gè)狀態(tài):

  • pending:等待中
  • resolved:已成功
  • rejected:已失敗

Promise 的狀態(tài)改變只有兩種可能:從 pending 變?yōu)?resolved 或者從 pending 變?yōu)?rejected,如下圖(引自 Promise 迷你書):

引自 Promise 迷你書

而且需要注意的是一旦狀態(tài)改變,狀態(tài)不會(huì)再變了,接下來就一直是這個(gè)結(jié)果。也就是說當(dāng)我們?cè)?executor 函數(shù)中調(diào)用了 resolve 之后,之后調(diào)用 reject 就沒有效果了,反之亦然。

// 并在 executor 函數(shù)中執(zhí)行業(yè)務(wù)邏輯
function executor(resolve, reject){
  resolve(100);
  // 之后調(diào)用 resolve,reject 都是無效的,
  // 因?yàn)闋顟B(tài)已經(jīng)變?yōu)?resolved,不會(huì)再改變了
  reject(100);
}

let x1 = new Promise(executor);

then

每一個(gè) promise 都一個(gè) then 方法,這個(gè)是當(dāng) promise 返回結(jié)果之后,需要執(zhí)行的回調(diào)函數(shù),他有兩個(gè)可選參數(shù):

  • onFulfilled:成功的回調(diào);
  • onRejected:失敗的回調(diào);

如下圖(引自 Promise 迷你書):

引自 Promise 迷你書
// ...
let x1 = new Promise(executor);

// x1 延遲綁定回調(diào)函數(shù) onResolve
function onResolved(value){
  console.log(value);
}

// x1 延遲綁定回調(diào)函數(shù) onRejected
function onRejected(reason){
  console.log(reason);
}

x1.then(onResolved, onRejected);

?

手寫 Promise 大致流程

在這里我們簡(jiǎn)單過一下手寫一個(gè) Promise 的大致流程:

executor 與三個(gè)狀態(tài)

  • new Promise 時(shí),需要傳遞一個(gè) executor 執(zhí)行器函數(shù),在構(gòu)造函數(shù)中,執(zhí)行器函數(shù)立刻執(zhí)行
  • executor 執(zhí)行函數(shù)接受兩個(gè)參數(shù),分別是 resolvereject
  • Promise 只能從 pendingrejected, 或者從 pendingfulfilled
  • Promise 的狀態(tài)一旦確認(rèn),狀態(tài)就凝固了,不在改變

then 方法

  • 所有的 Promise 都有 then 方法,then 接收兩個(gè)參數(shù),分別是 Promise 成功的回調(diào) onFulfilled,和失敗的回調(diào) onRejected
  • 如果調(diào)用 then 時(shí),Promise 已經(jīng)成功,則執(zhí)行 onFulfilled,并將 Promise 的值作為參數(shù)傳遞進(jìn)去;如果 Promise 已經(jīng)失敗,那么執(zhí)行 onRejected,并將 Promise 失敗的原因作為參數(shù)傳遞進(jìn)去;如果 Promise 的狀態(tài)是 pending,需要將 onFulfilledonRejected 函數(shù)存放起來,等待狀態(tài)確定后,再依次將對(duì)應(yīng)的函數(shù)執(zhí)行(觀察者模式)
  • then 的參數(shù) onFulfilledonRejected 可以不傳,Promise 可以進(jìn)行值穿透。

鏈?zhǔn)秸{(diào)用并處理 then 返回值

  • Promise 可以 then 多次,Promisethen 方法返回一個(gè)新的 Promise
  • 如果 then 返回的是一個(gè)正常值,那么就會(huì)把這個(gè)結(jié)果(value)作為參數(shù),傳遞給下一個(gè) then 的成功的回調(diào)(onFulfilled
  • 如果 then 中拋出了異常,那么就會(huì)把這個(gè)異常(reason)作為參數(shù),傳遞給下一個(gè) then 的失敗的回調(diào)(onRejected)
  • 如果 then 返回的是一個(gè) promise 或者其他 thenable 對(duì)象,那么需要等這個(gè) promise 執(zhí)行完撐,promise 如果成功,就走下一個(gè) then 的成功回調(diào);如果失敗,就走下一個(gè) then 的失敗回調(diào)。

上面是大致的實(shí)現(xiàn)流程,如果迷迷糊糊沒關(guān)系,只要大致有一個(gè)印象即可,后續(xù)我們會(huì)一一講到。

那接下來我們就開始實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的例子開始講解。

image

?

第一版(從一個(gè)簡(jiǎn)單例子開始)

我們先寫一個(gè)簡(jiǎn)單版,這版暫不支持狀態(tài)、鏈?zhǔn)秸{(diào)用,并且只支持調(diào)用一個(gè) then 方法。

來個(gè) ??

let p1 = new MyPromise((resolve, reject) => {
    setTimeout(() => {
      resolved('成功了');
    }, 1000);
})

p1.then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
})

例子很簡(jiǎn)單,就是 1s 之后返回 成功了,并在 then 中輸出。

實(shí)現(xiàn)

我們定義一個(gè) MyPromise 類,接著我們?cè)谄渲芯帉懘a,具體代碼如下:

class MyPromise {
  // ts 接口定義 ...
  constructor (executor: executor) {
    // 用于保存 resolve 的值
    this.value = null;
    // 用于保存 reject 的值
    this.reason = null;
    // 用于保存 then 的成功回調(diào)
    this.onFulfilled = null;
    // 用于保存 then 的失敗回調(diào)
    this.onRejected = null;

    // executor 的 resolve 參數(shù)
    // 用于改變狀態(tài) 并執(zhí)行 then 中的成功回調(diào)
    let resolve = value => {
      this.value = value;
      this.onFulfilled && this.onFulfilled(this.value);
    }

    // executor 的 reject 參數(shù)
    // 用于改變狀態(tài) 并執(zhí)行 then 中的失敗回調(diào)
    let reject = reason => {
      this.reason = reason;
      this.onRejected && this.onRejected(this.reason);
    }

    // 執(zhí)行 executor 函數(shù)
    // 將我們上面定義的兩個(gè)函數(shù)作為參數(shù) 傳入
    // 有可能在 執(zhí)行 executor 函數(shù)的時(shí)候會(huì)出錯(cuò),所以需要 try catch 一下 
    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }

  // 定義 then 函數(shù)
  // 并且將 then 中的參數(shù)復(fù)制給 this.onFulfilled 和 this.onRejected
  private then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }
}

好了,我們的第一版就完成了,是不是很簡(jiǎn)單。

不過這里需要注意的是,resolve 函數(shù)的執(zhí)行時(shí)機(jī)需要在 then 方法將回調(diào)函數(shù)注冊(cè)了之后,在 resolve 之后在去往賦值回調(diào)函數(shù),其實(shí)已經(jīng)完了,沒有任何意義。

上面的例子沒有問題,是因?yàn)?resolve(成功了) 是包在 setTimeout 中的,他會(huì)在下一個(gè)宏任務(wù)執(zhí)行,這時(shí)回調(diào)函數(shù)已經(jīng)注冊(cè)了。

大家可以試試把 resolve(成功了)setTimeout 中拿出來,這個(gè)時(shí)候就會(huì)出現(xiàn)問題了。

存在問題

這一版實(shí)現(xiàn)很簡(jiǎn)單,還存在幾個(gè)問題:

  • 未引入狀態(tài)的概念

未引入狀態(tài)的概念,現(xiàn)在狀態(tài)可以隨意變,不符合 Promise 狀態(tài)只能從等待態(tài)變化的規(guī)則。

  • 不支持鏈?zhǔn)秸{(diào)用

正常情況下我們可以對(duì) Promise 進(jìn)行鏈?zhǔn)秸{(diào)用:

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

p1.then(onResolved1, onRejected1).then(onResolved2, onRejected2)
  • 只支持一個(gè)回調(diào)函數(shù),如果存在多個(gè)回調(diào)函數(shù)的話,后面的會(huì)覆蓋前面的

在這個(gè)例子中,onResolved2 會(huì)覆蓋 onResolved1onRejected2 會(huì)覆蓋 onRejected1。

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

// 注冊(cè)多個(gè)回調(diào)函數(shù)
p1.then(onResolved1, onRejected1);
p1.then(onResolved2, onRejected2);

接下來我們更進(jìn)一步,把這些問題給解決掉。

image

?

第二版(實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用)

這一版我們把狀態(tài)的概念引入,同時(shí)實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用的功能。

加上狀態(tài)

上面我們說到 Promise 有三個(gè)狀態(tài):pending、resovled、rejected,只能從 pending 轉(zhuǎn)為 resovled 或者 rejected,而且當(dāng)狀態(tài)改變之后,狀態(tài)就不能再改變了。

  • 我們定義一個(gè)屬性 status:用于記錄當(dāng)前 Promise 的狀態(tài)
  • 為了防止寫錯(cuò),我們把狀態(tài)定義成常量 PENDING、RESOLVED、REJECTED。
  • 同時(shí)我們將保存 then 的成功回調(diào)定義為一個(gè)數(shù)組:this.resolvedQueuesthis.rejectedQueues,我們可以把 then 中的回調(diào)函數(shù)都塞入對(duì)應(yīng)的數(shù)組中,這樣就能解決我們上面提到的第三個(gè)問題。
class MyPromise {
  private static PENDING = 'pending';
  private static RESOLVED = 'resolved';
  private static REJECTED = 'rejected';

  constructor (executor: executor) {
    this.status = MyPromise.PENDING;
    // ...

    // 用于保存 then 的成功回調(diào)數(shù)組
    this.resolvedQueues = [];
    // 用于保存 then 的失敗回調(diào)數(shù)組
    this.rejectedQueues = [];

    let resolve = value => {
      // 當(dāng)狀態(tài)是 pending 是,將 promise 的狀態(tài)改為成功態(tài)
      // 同時(shí)遍歷執(zhí)行 成功回調(diào)數(shù)組中的函數(shù),將 value 傳入
      if (this.status == MyPromise.PENDING) {
        this.value = value;
        this.status = MyPromise.RESOLVED;
        this.resolvedQueues.forEach(cb => cb(this.value))
      }
    }

    let reject = reason => {
      // 當(dāng)狀態(tài)是 pending 是,將 promise 的狀態(tài)改為失敗態(tài)
      // 同時(shí)遍歷執(zhí)行 失敗回調(diào)數(shù)組中的函數(shù),將 reason 傳入
      if (this.status == MyPromise.PENDING) {
        this.reason = reason;
        this.status = MyPromise.REJECTED;
        this.rejectedQueues.forEach(cb => cb(this.reason))
      }
    }

    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }
}

完善 then 函數(shù)

接著我們來完善 then 中的方法,之前我們是直接將 then 的兩個(gè)參數(shù) onFulfilledonRejected,直接賦值給了 Promise 的用于保存成功、失敗函數(shù)回調(diào)的實(shí)例屬性。

現(xiàn)在我們需要將這兩個(gè)屬性塞入到兩個(gè)數(shù)組中去:resolvedQueuesrejectedQueues。

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // 首先判斷兩個(gè)參數(shù)是否為函數(shù)類型,因?yàn)檫@兩個(gè)參數(shù)是可選參數(shù)
    // 當(dāng)參數(shù)不是函數(shù)類型時(shí),需要?jiǎng)?chuàng)建一個(gè)函數(shù)賦值給對(duì)應(yīng)的參數(shù)
    // 這也就實(shí)現(xiàn)了 透?jìng)?    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}

    // 當(dāng)狀態(tài)是等待態(tài)的時(shí)候,需要將兩個(gè)參數(shù)塞入到對(duì)應(yīng)的回調(diào)數(shù)組中
    // 當(dāng)狀態(tài)改變之后,在執(zhí)行回調(diào)函數(shù)中的函數(shù)
    if (this.status === MyPromise.PENDING) {
      this.resolvedQueues.push(onFulfilled)
      this.rejectedQueues.push(onRejected)
    }

    // 狀態(tài)是成功態(tài),直接就調(diào)用 onFulfilled 函數(shù)
    if (this.status === MyPromise.RESOLVED) {
      onFulfilled(this.value)
    }

    // 狀態(tài)是成功態(tài),直接就調(diào)用 onRejected 函數(shù)
    if (this.status === MyPromise.REJECTED) {
      onRejected(this.reason)
    }
  }
}

then 函數(shù)的一些說明

  • 什么情況下 this.status 會(huì)是 pending 狀態(tài),什么情況下會(huì)是 resolved 狀態(tài)

這個(gè)其實(shí)也和事件循環(huán)機(jī)制有關(guān),如下代碼:

// this.status 為 pending 狀態(tài)
new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve(1)
  }, 0)
}).then(value => {
  console.log(value)
})

// this.status 為 resolved 狀態(tài)
new MyPromise((resolve, reject) => {
  resolve(1)
}).then(value => {
  console.log(value)
})
  • 什么是 透?jìng)?/strong>

如下面代碼,當(dāng) then 中沒有傳任何參數(shù)的時(shí)候,Promise 會(huì)使用內(nèi)部默認(rèn)的定義的方法,將結(jié)果傳遞給下一個(gè) then。

let p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolved('成功了');
  }, 1000);
})

p1.then().then((res) => {
  console.log(res);
})

因?yàn)槲覀儸F(xiàn)在還沒支持鏈?zhǔn)秸{(diào)用,這段代碼運(yùn)行會(huì)出問題。

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

支持鏈?zhǔn)秸{(diào)用,其實(shí)很簡(jiǎn)單,我們只需要給 then 函數(shù)最后返回 this 就行,這樣就支持了鏈?zhǔn)秸{(diào)用:

class MyPromise {
  // ...
  private then(onFulfilled, onRejected) {
    // ...
    return this;
  }
}

每次調(diào)用 then 之后,我們都返回當(dāng)前的這個(gè) Promise 對(duì)象,因?yàn)?Promise 對(duì)象上是存在 then 方法的,這個(gè)時(shí)候我們就簡(jiǎn)單的實(shí)現(xiàn)了 Promise 的簡(jiǎn)單調(diào)用。

這個(gè)時(shí)候運(yùn)行上面 透?jìng)?/strong> 的測(cè)試代碼了。

但是上面的代碼還是存在相應(yīng)的問題的,看下面代碼:

const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');  
});

p1.then((res) => {
  console.log(res);
  return 'then1';
})
.then((res) => {
  console.log(res);
  return 'then2';
})
.then((res) => {
  console.log(res);
  return 'then3';
})

// 預(yù)測(cè)輸出:resolved -> then1 -> then2
// 實(shí)際輸出:resolved -> resolved -> resolved

輸出與我們的預(yù)期有偏差,因?yàn)槲覀?then 中返回的 this 代表了 p1,在 new MyPromise 之后,其實(shí)狀態(tài)已經(jīng)從 pending 態(tài)變?yōu)榱?resolved 態(tài),之后不會(huì)再變了,所以在 MyPromise 中的 this.value 值就一直是 resolved

這個(gè)時(shí)候我們就得看看關(guān)于 then 返回值的相關(guān)知識(shí)點(diǎn)了。

then 返回值

實(shí)際上 then 都會(huì)返回了一個(gè)新的 Promise 對(duì)象。

先看下面這段代碼:

// 新創(chuàng)建一個(gè) promise
const aPromise = new Promise(function (resolve) {
  resolve(100);
});

// then 返回的 promise
var thenPromise = aPromise.then(function (value) {
  console.log(value);
});

console.log(aPromise !== thenPromise); // => true

從上面的代碼中我們可以得出 then 方法返回的 Promise 已經(jīng)不再是最初的 Promise 了,如下圖(引自 Promise 迷你書):

引自 Promise 迷你書

promise 的鏈?zhǔn)秸{(diào)用跟 jQuery 的鏈?zhǔn)秸{(diào)用是有區(qū)別的,jQuery 鏈?zhǔn)秸{(diào)用返回的對(duì)象還是最初那個(gè) jQuery 對(duì)象;Promise 更類似于數(shù)組中一些方法,如 slice,每次進(jìn)行操作之后,都會(huì)返回一個(gè)新的值。

改造代碼

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason}

    // then 方法返回一個(gè)新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功狀態(tài),直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 將 onFulfilled 函數(shù)的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolve(x);
      }

      // 失敗狀態(tài),直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 將 onRejected 函數(shù)的返回值,reject 出去
        let x = onRejected(this.reason)
        reject && reject(x);
      }

      // 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push((value) => {
          let x = onFulfilled(value);
          resolve(x);
        })
        this.rejectedQueues.push((reason) => {
          let x = onRejected(reason);
          reject && reject(x);
        })
      }
    });
    return promise2;
  }
}

// 輸出結(jié)果 resolved -> then1 -> then2

存在問題

到這里我們就完成了簡(jiǎn)單的鏈?zhǔn)秸{(diào)用,但是只能支持同步的鏈?zhǔn)秸{(diào)用,如果我們需要在 then 方法中再去進(jìn)行其他異步操作的話,上面的代碼就 GG 了。

如下代碼:

const p1 = new MyPromise((resolved, rejected) => {
  resolved('我 resolved 了');  
});

p1.then((res) => {
  console.log(res);
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then1');
    }, 1000)
  });
})
.then((res) => {
  console.log(res);
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  console.log(res);
  return 'then3';
})

上面的代碼會(huì)直接將 Promise 對(duì)象直接當(dāng)作參數(shù)傳給下一個(gè) then 函數(shù),而我們其實(shí)是想要將這個(gè) Promise 的處理結(jié)果傳遞下去。

image

?

第三版(異步鏈?zhǔn)秸{(diào)用)

這一版我們來實(shí)現(xiàn) promise 的異步鏈?zhǔn)秸{(diào)用。

思路

先看一下 thenonFulfilledonRejected 返回的值:

// 成功的函數(shù)返回
let x = onFulfilled(this.value);

// 失敗的函數(shù)返回
let x = onRejected(this.reason);

從上面的的問題中可以看出,x 可以是一個(gè) 普通值,也可以是一個(gè) Promise 對(duì)象,普通值的傳遞我們?cè)?第二版 已經(jīng)解決了,現(xiàn)在需要解決的是當(dāng) x 返回一個(gè) Promise 對(duì)象的時(shí)候該怎么處理。

其實(shí)也很簡(jiǎn)單,當(dāng) x 是一個(gè) Promise 對(duì)象的時(shí)候,我們需要進(jìn)行等待,直到返回的 Promise 狀態(tài)變化的時(shí)候,再去執(zhí)行之后的 then 函數(shù),代碼如下:

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}

    // then 方法返回一個(gè)新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功狀態(tài),直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 將 onFulfilled 函數(shù)的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

      // 失敗狀態(tài),直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 將 onRejected 函數(shù)的返回值,reject 出去
        let x = onRejected(this.reason)
        resolvePromise(promise2, x, resolve, reject);
      }

      // 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push(() => {
          let x = onFulfilled(this.value);
          resolvePromise(promise2, x, resolve, reject);
        })
        this.rejectedQueues.push(() => {
          let x = onRejected(this.reason);
          resolvePromise(promise2, x, resolve, reject);
        })
      }
    });
    return promise2;
  }
}

我們新寫一個(gè)函數(shù) resolvePromise,這個(gè)函數(shù)是用來處理異步鏈?zhǔn)秸{(diào)用的核心方法,他會(huì)去判斷 x 返回值是不是 Promise 對(duì)象,如果是的話,就直到 Promise 返回成功之后在再改變狀態(tài),如果是普通值的話,就直接將這個(gè)值 resovle 出去:

const resolvePromise = (promise2, x, resolve, reject) => {
  if (x instanceof MyPromise) {
    const then = x.then;
    if (x.status == MyPromise.PENDING) {
      then.call(x, y => {
        resolvePromise(promise2, y, resolve, reject);
      }, err => {
        reject(err);
      })
    } else {
      x.then(resolve, reject);
    }
  } else {
    resolve(x);
  }
}

代碼說明

resolvePromise

resolvePromise 接受四個(gè)參數(shù):

  • promise2then 中返回的 promise;
  • xthen 的兩個(gè)參數(shù) onFulfilled 或者 onRejected 的返回值,類型不確定,有可能是普通值,有可能是 thenable 對(duì)象;
  • resolverejectpromise2 的。

then 返回值類型

當(dāng) xPromise 的時(shí),并且他的狀態(tài)是 Pending 狀態(tài),如果 x 執(zhí)行成功,那么就去遞歸調(diào)用 resolvePromise 這個(gè)函數(shù),將 x 執(zhí)行結(jié)果作為 resolvePromise 第二個(gè)參數(shù)傳入;

如果執(zhí)行失敗,則直接調(diào)用 promise2reject 方法。

?

到這里我們基本上一個(gè)完整的 promise,接下來我們需要根據(jù) Promises/A+ 來規(guī)范一下我們的 Promise。

?

規(guī)范 Promise

前幾版的代碼筆者基本上是按照規(guī)范來的,這里主要講幾個(gè)沒有符合規(guī)范的點(diǎn)。

規(guī)范 then(規(guī)范 2.2)

thenonFulfilledonRejected 需要異步執(zhí)行,即放到異步任務(wù)中去執(zhí)行(規(guī)范 2.2.4)

實(shí)現(xiàn)

我們需要將 then 中的函數(shù)通過 setTimeout 包裹起來,放到一個(gè)宏任務(wù)中去,這里涉及了 jsEventLoop,大家可以去看看相應(yīng)的文章,如下:

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // ...
    // then 方法返回一個(gè)新的 promise
    const promise2 = new MyPromise((resolve, reject) => {
      // 成功狀態(tài),直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 將 onFulfilled 函數(shù)的返回值,resolve 出去
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err);
          }
        })
      }

      // 失敗狀態(tài),直接 reject
      if (this.status === MyPromise.REJECTED) {
        // 將 onRejected 函數(shù)的返回值,reject 出去
        setTimeout(() => {
          try {
            let x = onRejected(this.reason)
            resolvePromise(promise2, x, resolve, reject);
          } catch(err) {
            reject(err);
          }
        })
      }

      // 等待狀態(tài),將 onFulfilled,onRejected 塞入數(shù)組中,等待回調(diào)執(zhí)行
      if (this.status === MyPromise.PENDING) {
        this.resolvedQueues.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch(err) {
              reject(err);
            }
          })
        })
        this.rejectedQueues.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason)
              resolvePromise(promise2, x, resolve, reject);
            } catch(err) {
              reject(err);
            }
          })
        })
      }
    });
    return promise2;
  }
}

使用微任務(wù)包裹

但這樣還是有一個(gè)問題,我們知道其實(shí) Promise.then 是屬于微任務(wù)的,現(xiàn)在當(dāng)使用 setTimeout 包裹之后,就相當(dāng)于會(huì)變成一個(gè)宏任務(wù),可以看下面這一個(gè)例子:

var p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

setTimeout(() => {
  console.log('---setTimeout---');
}, 0);

p1.then(res => {
  console.log('---then---');
})

// 正常 Promise:then -> setTimeout
// 我們的 Promise:setTimeout -> then

輸出順序不一樣,原因是因?yàn)楝F(xiàn)在的 Promise 是通過 setTimeout 宏任務(wù)包裹的。

我們可以改進(jìn)一下,使用微任務(wù)來包裹 onFulfilled 、onRejected,常用的微任務(wù)有 process.nextTick、MutationObserver、postMessage 等,我們這個(gè)使用 postMessage 改寫一下:

// ...
if (this.status === MyPromise.RESOLVED) {
  // 將 onFulfilled 函數(shù)的返回值,resolve 出去
  // 注冊(cè)一個(gè) message 事件
  window.addEventListener('message', event => {
    const { type, data } =  event.data;

    if (type === '__promise') {
      try {
        let x = onFulfilled(that.value);
        resolvePromise(promise2, x, resolve, reject);
      } catch(err) {
        reject(err);
      }
    }
  });
  // 立馬執(zhí)行
  window.postMessage({
    type: '__promise',
  }, "http://localhost:3001");
}

// ...

實(shí)現(xiàn)方法很簡(jiǎn)單,我們監(jiān)聽windowmessage 事件,并在之后立馬觸發(fā)一個(gè) postMessage 事件,這個(gè)時(shí)候其實(shí) then 中的回調(diào)函數(shù)已經(jīng)在微任務(wù)隊(duì)列中了,我們重新運(yùn)行一下例子,可以看到輸出的順序變?yōu)榱?then -> setTimeout

當(dāng)然 Promise 內(nèi)部實(shí)現(xiàn)肯定沒有這么簡(jiǎn)單,筆者在這里只是提供一種思路,大家有興趣可以去研究一波。

規(guī)范 resolvePromise 函數(shù)(規(guī)范 2.3)

重復(fù)引用

重復(fù)引用,當(dāng) xpromise2 是一樣的,那就需要報(bào)一個(gè)錯(cuò)誤,重復(fù)應(yīng)用。(規(guī)范 2.3.1)
<br />
<br /> 因?yàn)樽约旱却约和瓿墒怯肋h(yuǎn)都不會(huì)有結(jié)果的。

const p1 = new MyPromise((resolved, rejected) => {
  resolved('我 resolved 了');  
});

const p2 = p1.then((res) => {
  return p2;
});
image

x 的類型

大致分為一下這么幾條:

  • 2.3.2:當(dāng) x 是一個(gè) Promise,那么就等待 x 改變狀態(tài)之后,才算完成或者失?。ㄟ@個(gè)也屬于 2.3.3,因?yàn)?Promise 其實(shí)也是一個(gè) thenable 對(duì)象)
  • 2.3.3:當(dāng) x 是一個(gè)對(duì)象 或者 函數(shù)的時(shí)候,即 thenable 對(duì)象,那就那 x.then 作為 then
  • 2.3.4:當(dāng) x 不是一個(gè)對(duì)象,或者函數(shù)的時(shí)候,直接將 x 作為參數(shù) resolve 返回。

我們主要看一下 2.3.3 就行,因?yàn)?Prmise 也屬于 thenable 對(duì)象,那什么是 thenable 對(duì)象呢?

簡(jiǎn)單來說就是具有 then方法的對(duì)象/函數(shù),所有的 Promise 對(duì)象都是 thenable 對(duì)象,但并非所有的 thenable 對(duì)象并非是 Promise 對(duì)象。如下:

let thenable = {
 then: function(resolve, reject) {
   resolve(100);
 }
}

根據(jù) x 的類型進(jìn)行處理:

  • 如果 x 不是 thenable 對(duì)象,直接調(diào)用 Promise2resolve,將 x 作為成功的結(jié)果;

  • 當(dāng) xthenable 對(duì)象,會(huì)調(diào)用 xthen 方法,成功后再去調(diào)用 resolvePromise 函數(shù),并將執(zhí)行結(jié)果 y 作為新的 x 傳入 resolvePromise,直到這個(gè) x 值不再是一個(gè) thenable 對(duì)象為止;如果失敗則直接調(diào)用 promise2reject

if (x != null && (typeof x === 'object' || typeof x === 'function')) {
  if (typeof then === 'function') {
    then.call(x, (y) => {
      resolvePromise(promise2, y, resolve, reject);
    }, (err) => {
      reject(err);
    })
  }
} else {
  resolve(x);
}

只調(diào)用一次

規(guī)范(Promise/A+ 2.3.3.3.3)規(guī)定如果同時(shí)調(diào)用 resolvePromiserejectPromise,或者對(duì)同一參數(shù)進(jìn)行了多次調(diào)用,則第一個(gè)調(diào)用優(yōu)先,而所有其他調(diào)用均被忽略,確保只執(zhí)行一次改變狀態(tài)。

我們?cè)谕饷娑x了一個(gè) called 占位符,為了獲得 then 函數(shù)有沒有執(zhí)行過相應(yīng)的改變狀態(tài)的函數(shù),執(zhí)行過了之后,就不再去執(zhí)行了,主要就是為了滿足規(guī)范。

x 為 Promise 對(duì)象

如果 xPromise 對(duì)象的話,其實(shí)當(dāng)執(zhí)行了resolve 函數(shù) 之后,就不會(huì)再執(zhí)行 reject 函數(shù)了,是直接在當(dāng)前這個(gè) Promise 對(duì)象就結(jié)束掉了。

x 為 thenable 對(duì)象

當(dāng) x 是普通的 thenable 函數(shù)的時(shí)候,他就有可能同時(shí)執(zhí)行 resolvereject 函數(shù),即可以同時(shí)執(zhí)行 promise2resolve 函數(shù) 和 reject 函數(shù),但是其實(shí) promise2 在狀態(tài)改變了之后,也不會(huì)再改變相應(yīng)的值了。其實(shí)也沒有什么問題,如下代碼:

// thenable 對(duì)像
{
 then: function(resolve, reject) {
   setTimeout(() => {
     resolve('我是thenable對(duì)像的 resolve');
     reject('我是thenable對(duì)像的 reject')
    })
 }
}

完整的 resolvePromise

完整的 resolvePromise 函數(shù)如下:

const resolvePromise = (promise2, x, resolve, reject) => {
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}

到這里就大功告成了,開不開心,興不興奮!

image

最后我們可以通過測(cè)試腳本跑一下我們的 MyPromise 是否符合規(guī)范。

測(cè)試

有專門的測(cè)試腳本(promises-aplus-tests)可以幫助我們測(cè)試所編寫的代碼是否符合 Promise/A+ 的規(guī)范。

但是貌似只能測(cè)試 js 文件,所以筆者就將 ts 文件轉(zhuǎn)化為了 js 文件,進(jìn)行測(cè)試

在代碼里面加上:

// 執(zhí)行測(cè)試用例需要用到的代碼
MyPromise.deferred = function() {
  let defer = {};
  defer.promise = new MyPromise((resolve, reject) => {
      defer.resolve = resolve;
      defer.reject = reject;
  });
  return defer;
}

需要提前安裝一下測(cè)試插件:

# 安裝測(cè)試腳本
npm i -g promises-aplus-tests

# 開始測(cè)試
promises-aplus-tests MyPromise.js

結(jié)果如下:

image

完美通過,接下去我們就可以看看 Promise 更多方法的實(shí)現(xiàn)了。

?

更多方法

實(shí)現(xiàn)上面的 Promise 之后,其實(shí)編寫其實(shí)例和靜態(tài)方法,相對(duì)來說就簡(jiǎn)單了很多。

實(shí)例方法

Promise.prototype.catch

實(shí)現(xiàn)

其實(shí)這個(gè)方法就是 then 方法的語法糖,只需要給 then 傳遞 onRejected 參數(shù)就 ok 了。

private catch(onRejected) {
  return this.then(null, onRejected);
}
例子:
const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

p1.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      rejected('錯(cuò)誤了');
    }, 1000)
  });
})
.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  return 'then3';
}).catch(error => {
  console.log('----error', error);
})

// 1s 之后輸出:----error 錯(cuò)誤了

Promise.prototype.finally

實(shí)現(xiàn)

finally() 方法用于指定不管 Promise 對(duì)象最后狀態(tài)如何,都會(huì)執(zhí)行的操作。

private finally (fn) {
  return this.then(fn, fn);
}
例子
const p1 = new MyPromise((resolved, rejected) => {
  resolved('resolved');
})

p1.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      rejected('錯(cuò)誤了');
    }, 1000)
  });
})
.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  return 'then3';
}).catch(error => {
  console.log('---error', error);
  return `catch-${error}`
}).finally(res => {
  console.log('---finally---', res);
})

// 輸出結(jié)果:---error 錯(cuò)誤了" -> ""---finally--- catch-錯(cuò)誤了

?

靜態(tài)方法

Promise.resolve

實(shí)現(xiàn)

有時(shí)需要將現(xiàn)有對(duì)象轉(zhuǎn)為 Promise 對(duì)象,Promise.resolve()方法就起到這個(gè)作用。

static resolve = (val) => {
  return new MyPromise((resolve,reject) => {
    resolve(val);
  });
}
例子
MyPromise.resolve({name: 'darrell', sex: 'boy' }).then((res) => {
  console.log(res);
}).catch((error) => {
  console.log(error);
});

// 輸出結(jié)果:{name: "darrell", sex: "boy"}

Promise.reject

實(shí)現(xiàn)

Promise.reject(reason) 方法也會(huì)返回一個(gè)新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為 rejected。

static reject = (val) => {
  return new MyPromise((resolve,reject) => {
    reject(val)
  });
}
例子
MyPromise.reject("出錯(cuò)了").then((res) => {
  console.log(res);
}).catch((error) => {
  console.log(error);
});

// 輸出結(jié)果:出錯(cuò)了

Promise.all

Promise.all()方法用于將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例,

const p = Promise.all([p1, p2, p3]);
  • 只有 p1p2、p3 的狀態(tài)都變成 fulfilled,p 的狀態(tài)才會(huì)變成 fulfilled
  • 只要 p1、p2、p3 之中有一個(gè)被 rejected,p 的狀態(tài)就變成 rejected,此時(shí)第一個(gè)被 reject 的實(shí)例的返回值,會(huì)傳遞給p的回調(diào)函數(shù)。
實(shí)現(xiàn)
static all = (promises: MyPromise[]) => {
  return new MyPromise((resolve, reject) => {
    let result: MyPromise[] = [];
    let count = 0;

    for (let i = 0; i < promises.length; i++) {
      promises[i].then(data => {
        result[i] = data;
        if (++count == promises.length) {
          resolve(result);
        }
      }, error => {
        reject(error);
      });
    }
  });
}
例子
let Promise1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('Promise1');
  }, 2000);
});

let Promise2 = new MyPromise((resolve, reject) => {
  resolve('Promise2');
});

let Promise3 = new MyPromise((resolve, reject) => {
  resolve('Promise3');
})

let Promise4 = new MyPromise((resolve, reject) => {
  reject('Promise4');
})

let p = MyPromise.all([Promise1, Promise2, Promise3, Promise4]);

p.then((res) => {
  // 三個(gè)都成功則成功  
  console.log('---成功了', res);
}).catch((error) => {
  // 只要有失敗,則失敗 
  console.log('---失敗了', err);
});

// 直接輸出:---失敗了 Promise4

Promise.race

Promise.race()方法同樣是將多個(gè) Promise 實(shí)例,包裝成一個(gè)新的 Promise 實(shí)例。

const p = Promise.race([p1, p2, p3]);

只要 p1、p2、p3 之中有一個(gè)實(shí)例率先改變狀態(tài),p 的狀態(tài)就跟著改變。那個(gè)率先改變的 Promise 實(shí)例的返回值,就傳遞給 p 的回調(diào)函數(shù)。

實(shí)現(xiàn)
static race = (promises) => {
  return new Promise((resolve,reject)=>{
    for(let i = 0; i < promises.length; i++){
      promises[i].then(resolve,reject)
    };
  })
}
例子

例子和 all 一樣,調(diào)用如下:

// ...

let p = MyPromise.race([Promise1, Promise2, Promise3, Promise4])

p.then((res) => { 
  console.log('---成功了', res);
}).catch((error) => {
  console.log('---失敗了', err);
});

// 直接輸出:---成功了 Promise2

Promise.allSettled

此方法接受一組 Promise 實(shí)例作為參數(shù),包裝成一個(gè)新的 Promise 實(shí)例。

const p = Promise.race([p1, p2, p3]);

只有等到所有這些參數(shù)實(shí)例都返回結(jié)果,不管是 fulfilled 還是 rejected,而且該方法的狀態(tài)只可能變成 fulfilled

此方法與 Promise.all 的區(qū)別是 all 無法確定所有請(qǐng)求都結(jié)束,因?yàn)樵?all 中,如果有一個(gè)被 Promiserejected,p 的狀態(tài)就立馬變成 rejected,有可能有些異步請(qǐng)求還沒走完。

實(shí)現(xiàn)
static allSettled = (promises: MyPromise[]) => {
  return new MyPromise((resolve) => {
    let result: MyPromise[] = [];
    let count = 0;
    for (let i = 0; i < promises.length; i++) {
      promises[i].finally(res => {
        result[i] = res;
        if (++count == promises.length) {
          resolve(result);
        }
      })
    }
  });
}
例子

例子和 all 一樣,調(diào)用如下:

let p = MyPromise.allSettled([Promise1, Promise2, Promise3, Promise4])

p.then((res) => {
  // 三個(gè)都成功則成功  
  console.log('---成功了', res);
}, err => {
  // 只要有失敗,則失敗 
  console.log('---失敗了', err);
})

// 2s 后輸出:---成功了 (4) ["Promise1", "Promise2", "Promise3", "Promise4"]

?

總結(jié)

這篇文章筆者帶大家一步一步的實(shí)現(xiàn)了符合 Promise/A+ 規(guī)范的的 Promise,看完之后相信大家基本上也能夠自己獨(dú)立寫出一個(gè) Promise 來了。

最后通過幾個(gè)問題,大家可以看看自己掌握的如何:

  • Promise 中是如何實(shí)現(xiàn)回調(diào)函數(shù)返回值穿透的?
  • Promise 出錯(cuò)后,是怎么通過 冒泡 傳遞給最后那個(gè)捕獲異常的函數(shù)?
  • Promise 如何支持鏈?zhǔn)秸{(diào)用?
  • 怎么將 Promise.then 包裝成一個(gè)微任務(wù)?

實(shí)不相瞞,想要個(gè)贊!

image

?

參考文檔

?

示例代碼

示例代碼可以看這里:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容