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

img2.jpeg

前言

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

?

Promise 的作用

PromiseJavaScript 異步編程的一種流行解決方案,它的出現(xiàn)是為了解決 回調(diào)地獄 的問題,讓使用者可以通過鏈式的寫法去編寫寫異步代碼,具體的用法筆者就不介紹了,大家可以參考阮一峰老師的 ES6 Promise教程。

?

課前知識

觀察者模式

什么是觀察者模式:

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

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

事件循環(huán)機制

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

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

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+ 是一個社區(qū)規(guī)范,如果你想寫出一個規(guī)范的 Promise,我們就需要遵循這個標準。之后我們也會根據(jù)規(guī)范來完善我們自己編寫的 Promise

?

Promise 核心知識點

在動手寫 Promise 之前,我們先過一下幾個重要的知識點。

executor

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

let x1 = new Promise(executor);

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

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

三個狀態(tài)

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

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

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

引自 Promise 迷你書

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

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

let x1 = new Promise(executor);

then

每一個 promise 都一個 then 方法,這個是當 promise 返回結(jié)果之后,需要執(zhí)行的回調(diào)函數(shù),他有兩個可選參數(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 大致流程

在這里我們簡單過一下手寫一個 Promise 的大致流程:

executor 與三個狀態(tài)

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

then 方法

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

鏈式調(diào)用并處理 then 返回值

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

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

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

image

?

第一版(從一個簡單例子開始)

我們先寫一個簡單版,這版暫不支持狀態(tài)、鏈式調(diào)用,并且只支持調(diào)用一個 then 方法。

來個 ??

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

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

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

實現(xiàn)

我們定義一個 MyPromise 類,接著我們在其中編寫代碼,具體代碼如下:

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ù)
    // 將我們上面定義的兩個函數(shù)作為參數(shù) 傳入
    // 有可能在 執(zhí)行 executor 函數(shù)的時候會出錯,所以需要 try catch 一下 
    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }

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

好了,我們的第一版就完成了,是不是很簡單。

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

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

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

存在問題

這一版實現(xiàn)很簡單,還存在幾個問題:

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

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

  • 不支持鏈式調(diào)用

正常情況下我們可以對 Promise 進行鏈式調(diào)用:

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

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

在這個例子中,onResolved2 會覆蓋 onResolved1,onRejected2 會覆蓋 onRejected1。

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

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

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

image

?

第二版(實現(xiàn)鏈式調(diào)用)

這一版我們把狀態(tài)的概念引入,同時實現(xiàn)鏈式調(diào)用的功能。

加上狀態(tài)

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

  • 我們定義一個屬性 status:用于記錄當前 Promise 的狀態(tài)
  • 為了防止寫錯,我們把狀態(tài)定義成常量 PENDING、RESOLVEDREJECTED。
  • 同時我們將保存 then 的成功回調(diào)定義為一個數(shù)組:this.resolvedQueuesthis.rejectedQueues,我們可以把 then 中的回調(diào)函數(shù)都塞入對應的數(shù)組中,這樣就能解決我們上面提到的第三個問題。
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 => {
      // 當狀態(tài)是 pending 是,將 promise 的狀態(tài)改為成功態(tài)
      // 同時遍歷執(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 => {
      // 當狀態(tài)是 pending 是,將 promise 的狀態(tài)改為失敗態(tài)
      // 同時遍歷執(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 的兩個參數(shù) onFulfilledonRejected,直接賦值給了 Promise 的用于保存成功、失敗函數(shù)回調(diào)的實例屬性。

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

class MyPromise {
  // ...

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

    // 當狀態(tài)是等待態(tài)的時候,需要將兩個參數(shù)塞入到對應的回調(diào)數(shù)組中
    // 當狀態(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 會是 pending 狀態(tài),什么情況下會是 resolved 狀態(tài)

這個其實也和事件循環(huán)機制有關(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)
})
  • 什么是 透傳

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

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

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

因為我們現(xiàn)在還沒支持鏈式調(diào)用,這段代碼運行會出問題。

支持鏈式調(diào)用

支持鏈式調(diào)用,其實很簡單,我們只需要給 then 函數(shù)最后返回 this 就行,這樣就支持了鏈式調(diào)用:

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

每次調(diào)用 then 之后,我們都返回當前的這個 Promise 對象,因為 Promise 對象上是存在 then 方法的,這個時候我們就簡單的實現(xiàn)了 Promise 的簡單調(diào)用。

這個時候運行上面 透傳 的測試代碼了。

但是上面的代碼還是存在相應的問題的,看下面代碼:

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';
})

// 預測輸出:resolved -> then1 -> then2
// 實際輸出:resolved -> resolved -> resolved

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

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

then 返回值

實際上 then 都會返回了一個新的 Promise 對象。

先看下面這段代碼:

// 新創(chuàng)建一個 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 的鏈式調(diào)用跟 jQuery 的鏈式調(diào)用是有區(qū)別的,jQuery 鏈式調(diào)用返回的對象還是最初那個 jQuery 對象;Promise 更類似于數(shù)組中一些方法,如 slice,每次進行操作之后,都會返回一個新的值。

改造代碼

class MyPromise {
  // ...

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

    // then 方法返回一個新的 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

存在問題

到這里我們就完成了簡單的鏈式調(diào)用,但是只能支持同步的鏈式調(diào)用,如果我們需要在 then 方法中再去進行其他異步操作的話,上面的代碼就 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';
})

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

image

?

第三版(異步鏈式調(diào)用)

這一版我們來實現(xiàn) promise 的異步鏈式調(diào)用。

思路

先看一下 thenonFulfilledonRejected 返回的值:

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

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

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

其實也很簡單,當 x 是一個 Promise 對象的時候,我們需要進行等待,直到返回的 Promise 狀態(tài)變化的時候,再去執(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 方法返回一個新的 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;
  }
}

我們新寫一個函數(shù) resolvePromise,這個函數(shù)是用來處理異步鏈式調(diào)用的核心方法,他會去判斷 x 返回值是不是 Promise 對象,如果是的話,就直到 Promise 返回成功之后在再改變狀態(tài),如果是普通值的話,就直接將這個值 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 接受四個參數(shù):

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

then 返回值類型

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

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

?

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

?

規(guī)范 Promise

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

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

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

實現(xiàn)

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

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // ...
    // then 方法返回一個新的 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;
  }
}

使用微任務包裹

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

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

輸出順序不一樣,原因是因為現(xiàn)在的 Promise 是通過 setTimeout 宏任務包裹的。

我們可以改進一下,使用微任務來包裹 onFulfilled 、onRejected,常用的微任務有 process.nextTick、MutationObserverpostMessage 等,我們這個使用 postMessage 改寫一下:

// ...
if (this.status === MyPromise.RESOLVED) {
  // 將 onFulfilled 函數(shù)的返回值,resolve 出去
  // 注冊一個 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");
}

// ...

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

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

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

重復引用

重復引用,當 xpromise2 是一樣的,那就需要報一個錯誤,重復應用。(規(guī)范 2.3.1)
<br />
<br /> 因為自己等待自己完成是永遠都不會有結(jié)果的。

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

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

x 的類型

大致分為一下這么幾條:

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

我們主要看一下 2.3.3 就行,因為 Prmise 也屬于 thenable 對象,那什么是 thenable 對象呢?

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

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

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

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

  • xthenable 對象,會調(diào)用 xthen 方法,成功后再去調(diào)用 resolvePromise 函數(shù),并將執(zhí)行結(jié)果 y 作為新的 x 傳入 resolvePromise,直到這個 x 值不再是一個 thenable 對象為止;如果失敗則直接調(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ī)定如果同時調(diào)用 resolvePromiserejectPromise,或者對同一參數(shù)進行了多次調(diào)用,則第一個調(diào)用優(yōu)先,而所有其他調(diào)用均被忽略,確保只執(zhí)行一次改變狀態(tài)。

我們在外面定義了一個 called 占位符,為了獲得 then 函數(shù)有沒有執(zhí)行過相應的改變狀態(tài)的函數(shù),執(zhí)行過了之后,就不再去執(zhí)行了,主要就是為了滿足規(guī)范。

x 為 Promise 對象

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

x 為 thenable 對象

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

// thenable 對像
{
 then: function(resolve, reject) {
   setTimeout(() => {
     resolve('我是thenable對像的 resolve');
     reject('我是thenable對像的 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

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

測試

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

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

在代碼里面加上:

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

需要提前安裝一下測試插件:

# 安裝測試腳本
npm i -g promises-aplus-tests

# 開始測試
promises-aplus-tests MyPromise.js

結(jié)果如下:

image

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

?

更多方法

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

實例方法

Promise.prototype.catch

實現(xiàn)

其實這個方法就是 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('錯誤了');
    }, 1000)
  });
})
.then((res) => {
  return new MyPromise((resolved, rejected) => {
    setTimeout(() => {
      resolved('then2');
    }, 1000)
  });
})
.then((res) => {
  return 'then3';
}).catch(error => {
  console.log('----error', error);
})

// 1s 之后輸出:----error 錯誤了

Promise.prototype.finally

實現(xiàn)

finally() 方法用于指定不管 Promise 對象最后狀態(tài)如何,都會執(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('錯誤了');
    }, 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 錯誤了" -> ""---finally--- catch-錯誤了

?

靜態(tài)方法

Promise.resolve

實現(xiàn)

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

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

實現(xiàn)

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

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

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

Promise.all

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

const p = Promise.all([p1, p2, p3]);
  • 只有 p1、p2p3 的狀態(tài)都變成 fulfilled,p 的狀態(tài)才會變成 fulfilled
  • 只要 p1、p2、p3 之中有一個被 rejected,p 的狀態(tài)就變成 rejected,此時第一個被 reject 的實例的返回值,會傳遞給p的回調(diào)函數(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) => {
  // 三個都成功則成功  
  console.log('---成功了', res);
}).catch((error) => {
  // 只要有失敗,則失敗 
  console.log('---失敗了', err);
});

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

Promise.race

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

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

只要 p1、p2、p3 之中有一個實例率先改變狀態(tài),p 的狀態(tài)就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調(diào)函數(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ù),包裝成一個新的 Promise 實例。

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

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

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

實現(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) => {
  // 三個都成功則成功  
  console.log('---成功了', res);
}, err => {
  // 只要有失敗,則失敗 
  console.log('---失敗了', err);
})

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

?

總結(jié)

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

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

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

實不相瞞,想要個贊!

image

?

參考文檔

?

示例代碼

示例代碼可以看這里:

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

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