30分鐘,帶你實現一個符合規範的 Promise(巨詳細)

前言

關於 Promise 原理解析的優秀文章,在掘金上已經有很是多了。可是筆者老是處在 看了就會,一寫就廢 的狀態,這是筆者寫這篇文章的目的,爲了理一下 Promise 的編寫思路,從零開始手寫一波代碼,同時也方便本身往後回顧。javascript

 

Promise 的做用

PromiseJavaScript 異步編程的一種流行解決方案,它的出現是爲了解決 回調地獄 的問題,讓使用者能夠經過鏈式的寫法去編寫寫異步代碼,具體的用法筆者就不介紹了,你們能夠參考阮一峯老師的 ES6 Promise教程java

 

課前知識

觀察者模式

什麼是觀察者模式:git

觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個目標對象,當這個目標對象的狀態發生變化時,會通知全部觀察者對象,使它們可以自動更新。es6

Promise 是基於 觀察者的設計模式 實現的,then 函數要執行的函數會被塞入觀察者數組中,當 Promise 狀態變化的時候,就去執行觀察組數組中的全部函數。github

事件循環機制

實現 Promise 涉及到了 JavaScript 中的事件循環機制 EventLoop、以及宏任務和微任務的概念。面試

事件循環機制的流程圖以下:typescript

你們能夠看一下這段代碼:shell

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);
複製代碼

若是不能一會兒說出輸出結果,建議你們能夠先查閱一下 事件循環 的相關資料,在掘金中有不少優秀的文章。npm

Promises/A+ 規範

Promises/A+ 是一個社區規範,若是你想寫出一個規範的 Promise,咱們就須要遵循這個標準。以後咱們也會根據規範來完善咱們本身編寫的 Promise編程

 

Promise 核心知識點

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

executor

// 建立 Promise 對象 x1
// 並在 executor 函數中執行業務邏輯
function executor(resolve, reject){
  // 業務邏輯處理成功結果
  const value = ...;
  resolve(value);
  // 失敗結果
  // const reason = ...;
  // reject(reason);
}

let x1 = new Promise(executor);
複製代碼

首先 Promise 是一個類,它接收一個執行函數 executor,它接收兩個參數:resolvereject,這兩個參數是 Promise 內部定義的兩個函數,用來改變狀態並執行對應回調函數。

由於 Promise 自己是不知道執行結果失敗或者成功,它只是給異步操做提供了一個容器,實際上的控制權在使用者的手上,使用者能夠調用上面兩個參數告訴 Promise 結果是否成功,同時將業務邏輯處理結果(value/reason)做爲參數傳給 resolvereject 兩個函數,執行回調。

三個狀態

Promise 有三個狀態:

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

Promise 的狀態改變只有兩種可能:從 pending 變爲 resolved 或者從 pending 變爲 rejected,以下圖(引自 Promise 迷你書):

引自 Promise 迷你書

並且須要注意的是一旦狀態改變,狀態不會再變了,接下來就一直是這個結果。也就是說當咱們在 executor 函數中調用了 resolve 以後,以後調用 reject 就沒有效果了,反之亦然。

// 並在 executor 函數中執行業務邏輯
function executor(resolve, reject){
  resolve(100);
  // 以後調用 resolve,reject 都是無效的,
  // 由於狀態已經變爲 resolved,不會再改變了
  reject(100);
}

let x1 = new Promise(executor);
複製代碼

then

每個 promise 都一個 then 方法,這個是當 promise 返回結果以後,須要執行的回調函數,他有兩個可選參數:

  • onFulfilled:成功的回調;
  • onRejected:失敗的回調;

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

引自 Promise 迷你書

// ...
let x1 = new Promise(executor);

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

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

x1.then(onResolved, onRejected);
複製代碼

 

手寫 Promise 大體流程

在這裏咱們簡單過一下手寫一個 Promise 的大體流程:

executor 與三個狀態

  • new Promise 時,須要傳遞一個 executor 執行器函數,在構造函數中,執行器函數馬上執行
  • executor 執行函數接受兩個參數,分別是 resolvereject
  • Promise 只能從 pendingrejected, 或者從 pendingfulfilled
  • Promise 的狀態一旦確認,狀態就凝固了,不在改變

then 方法

  • 全部的 Promise 都有 then 方法,then 接收兩個參數,分別是 Promise 成功的回調 onFulfilled,和失敗的回調 onRejected
  • 若是調用 then 時,Promise 已經成功,則執行 onFulfilled,並將 Promise 的值做爲參數傳遞進去;若是 Promise 已經失敗,那麼執行 onRejected,並將 Promise 失敗的緣由做爲參數傳遞進去;若是 Promise 的狀態是 pending,須要將 onFulfilledonRejected 函數存放起來,等待狀態肯定後,再依次將對應的函數執行(觀察者模式)
  • then 的參數 onFulfilledonRejected 能夠不傳,Promise 能夠進行值穿透

鏈式調用並處理 then 返回值

  • Promise 能夠 then 屢次,Promisethen 方法返回一個新的 Promise
  • 若是 then 返回的是一個正常值,那麼就會把這個結果(value)做爲參數,傳遞給下一個 then 的成功的回調(onFulfilled
  • 若是 then 中拋出了異常,那麼就會把這個異常(reason)做爲參數,傳遞給下一個 then 的失敗的回調(onRejected)
  • 若是 then 返回的是一個 promise 或者其餘 thenable 對象,那麼須要等這個 promise 執行完撐,promise 若是成功,就走下一個 then 的成功回調;若是失敗,就走下一個 then 的失敗回調。

上面是大體的實現流程,若是迷迷糊糊不要緊,只要大體有一個印象便可,後續咱們會一一講到。

那接下來咱們就開始實現一個最簡單的例子開始講解。

 

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

咱們先寫一個簡單版,這版暫不支持狀態、鏈式調用,而且只支持調用一個 then 方法。

來個 🌰

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

p1.then((data) => {
    console.log(data);
}, (err) => {
    console.log(err);
})
複製代碼

例子很簡單,就是 1s 以後返回 成功了,並在 then 中輸出。

實現

咱們定義一個 MyPromise 類,接着咱們在其中編寫代碼,具體代碼以下:

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

    // executor 的 resolve 參數
    // 用於改變狀態 並執行 then 中的成功回調
    let resolve = value => {
      this.value = value;
      this.onFulfilled && this.onFulfilled(this.value);
    }

    // executor 的 reject 參數
    // 用於改變狀態 並執行 then 中的失敗回調
    let reject = reason => {
      this.reason = reason;
      this.onRejected && this.onRejected(this.reason);
    }

    // 執行 executor 函數
    // 將咱們上面定義的兩個函數做爲參數 傳入
    // 有可能在 執行 executor 函數的時候會出錯,因此須要 try catch 一下 
    try {
      executor(resolve, reject);
    } catch(err) {
      reject(err);
    }
  }

  // 定義 then 函數
  // 而且將 then 中的參數複製給 this.onFulfilled 和 this.onRejected
  private then(onFulfilled, onRejected) {
    this.onFulfilled = onFulfilled;
    this.onRejected = onRejected;
  }
}
複製代碼

好了,咱們的初版就完成了,是否是很簡單。

不過這裏須要注意的是,resolve 函數的執行時機須要在 then 方法將回調函數註冊了以後,在 resolve 以後在去往賦值回調函數,其實已經完了,沒有任何意義。

上面的例子沒有問題,是由於 resolve(成功了) 是包在 setTimeout 中的,他會在下一個宏任務執行,這時回調函數已經註冊了。

你們能夠試試把 resolve(成功了)setTimeout 中拿出來,這個時候就會出現問題了。

存在問題

這一版實現很簡單,還存在幾個問題:

  • 未引入狀態的概念

未引入狀態的概念,如今狀態能夠隨意變,不符合 Promise 狀態只能從等待態變化的規則。

  • 不支持鏈式調用

正常狀況下咱們能夠對 Promise 進行鏈式調用:

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

p1.then(onResolved1, onRejected1).then(onResolved2, onRejected2)
複製代碼
  • 只支持一個回調函數,若是存在多個回調函數的話,後面的會覆蓋前面的

在這個例子中,onResolved2 會覆蓋 onResolved1onRejected2 會覆蓋 onRejected1

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

// 註冊多個回調函數
p1.then(onResolved1, onRejected1);
p1.then(onResolved2, onRejected2);
複製代碼

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

 

第二版(實現鏈式調用)

這一版咱們把狀態的概念引入,同時實現鏈式調用的功能。

加上狀態

上面咱們說到 Promise 有三個狀態:pendingresovledrejected,只能從 pending 轉爲 resovled 或者 rejected,並且當狀態改變以後,狀態就不能再改變了。

  • 咱們定義一個屬性 status:用於記錄當前 Promise 的狀態
  • 爲了防止寫錯,咱們把狀態定義成常量 PENDINGRESOLVEDREJECTED
  • 同時咱們將保存 then 的成功回調定義爲一個數組:this.resolvedQueuesthis.rejectedQueues,咱們能夠把 then 中的回調函數都塞入對應的數組中,這樣就能解決咱們上面提到的第三個問題。
class MyPromise {
  private static PENDING = 'pending';
  private static RESOLVED = 'resolved';
  private static REJECTED = 'rejected';

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

    // 用於保存 then 的成功回調數組
    this.resolvedQueues = [];
    // 用於保存 then 的失敗回調數組
    this.rejectedQueues = [];

    let resolve = value => {
      // 當狀態是 pending 是,將 promise 的狀態改成成功態
      // 同時遍歷執行 成功回調數組中的函數,將 value 傳入
      if (this.status == MyPromise.PENDING) {
        this.value = value;
        this.status = MyPromise.RESOLVED;
        this.resolvedQueues.forEach(cb => cb(this.value))
      }
    }

    let reject = reason => {
      // 當狀態是 pending 是,將 promise 的狀態改成失敗態
      // 同時遍歷執行 失敗回調數組中的函數,將 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 函數

接着咱們來完善 then 中的方法,以前咱們是直接將 then 的兩個參數 onFulfilledonRejected,直接賦值給了 Promise 的用於保存成功、失敗函數回調的實例屬性。

如今咱們須要將這兩個屬性塞入到兩個數組中去:resolvedQueuesrejectedQueues

class MyPromise {
  // ...

  private then(onFulfilled, onRejected) {
    // 首先判斷兩個參數是否爲函數類型,由於這兩個參數是可選參數
    // 當參數不是函數類型時,須要建立一個函數賦值給對應的參數
    // 這也就實現了 透傳
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason}

    // 當狀態是等待態的時候,須要將兩個參數塞入到對應的回調數組中
    // 當狀態改變以後,在執行回調函數中的函數
    if (this.status === MyPromise.PENDING) {
      this.resolvedQueues.push(onFulfilled)
      this.rejectedQueues.push(onRejected)
    }

    // 狀態是成功態,直接就調用 onFulfilled 函數
    if (this.status === MyPromise.RESOLVED) {
      onFulfilled(this.value)
    }

    // 狀態是成功態,直接就調用 onRejected 函數
    if (this.status === MyPromise.REJECTED) {
      onRejected(this.reason)
    }
  }
}
複製代碼

then 函數的一些說明

  • 什麼狀況下 this.status 會是 pending 狀態,什麼狀況下會是 resolved 狀態

這個其實也和事件循環機制有關,以下代碼:

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

// this.status 爲 resolved 狀態
new MyPromise((resolve, reject) => {
  resolve(1)
}).then(value => {
  console.log(value)
})
複製代碼
  • 什麼是 透傳

以下面代碼,當 then 中沒有傳任何參數的時候,Promise 會使用內部默認的定義的方法,將結果傳遞給下一個 then

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

p1.then().then((res) => {
  console.log(res);
})
複製代碼

由於咱們如今還沒支持鏈式調用,這段代碼運行會出問題。

支持鏈式調用

支持鏈式調用,其實很簡單,咱們只須要給 then 函數最後返回 this 就行,這樣就支持了鏈式調用:

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

每次調用 then 以後,咱們都返回當前的這個 Promise 對象,由於 Promise 對象上是存在 then 方法的,這個時候咱們就簡單的實現了 Promise 的簡單調用。

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

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

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 以後,其實狀態已經從 pending 態變爲了 resolved 態,以後不會再變了,因此在 MyPromise 中的 this.value 值就一直是 resolved

這個時候咱們就得看看關於 then 返回值的相關知識點了。

then 返回值

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

先看下面這段代碼:

// 新建立一個 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 已經再也不是最初的 Promise 了,以下圖(引自 Promise 迷你書):

引自 Promise 迷你書

promise 的鏈式調用跟 jQuery 的鏈式調用是有區別的,jQuery 鏈式調用返回的對象仍是最初那個 jQuery 對象;Promise 更相似於數組中一些方法,如 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) => {
      // 成功狀態,直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 將 onFulfilled 函數的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolve(x);
      }

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

      // 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
      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;
  }
}

// 輸出結果 resolved -> then1 -> then2
複製代碼

存在問題

到這裏咱們就完成了簡單的鏈式調用,可是隻能支持同步的鏈式調用,若是咱們須要在 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 對象直接看成參數傳給下一個 then 函數,而咱們實際上是想要將這個 Promise 的處理結果傳遞下去。

 

第三版(異步鏈式調用)

這一版咱們來實現 promise 的異步鏈式調用。

思路

先看一下 thenonFulfilledonRejected 返回的值:

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

// 失敗的函數返回
let x = onRejected(this.reason);
複製代碼

從上面的的問題中能夠看出,x 能夠是一個 普通值,也能夠是一個 Promise 對象,普通值的傳遞咱們在 第二版 已經解決了,如今須要解決的是當 x 返回一個 Promise 對象的時候該怎麼處理。

其實也很簡單,當 x 是一個 Promise 對象的時候,咱們須要進行等待,直到返回的 Promise 狀態變化的時候,再去執行以後的 then 函數,代碼以下:

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) => {
      // 成功狀態,直接 resolve
      if (this.status === MyPromise.RESOLVED) {
        // 將 onFulfilled 函數的返回值,resolve 出去
        let x = onFulfilled(this.value);
        resolvePromise(promise2, x, resolve, reject);
      }

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

      // 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
      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;
  }
}
複製代碼

咱們新寫一個函數 resolvePromise,這個函數是用來處理異步鏈式調用的核心方法,他會去判斷 x 返回值是否是 Promise 對象,若是是的話,就直到 Promise 返回成功以後在再改變狀態,若是是普通值的話,就直接將這個值 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 接受四個參數:

  • promise2then 中返回的 promise
  • xthen 的兩個參數 onFulfilled 或者 onRejected 的返回值,類型不肯定,有多是普通值,有多是 thenable 對象;
  • resolverejectpromise2 的。

then 返回值類型

xPromise 的時,而且他的狀態是 Pending 狀態,若是 x 執行成功,那麼就去遞歸調用 resolvePromise 這個函數,將 x 執行結果做爲 resolvePromise 第二個參數傳入;

若是執行失敗,則直接調用 promise2reject 方法。

 

到這裏咱們基本上一個完整的 promise,接下來咱們須要根據 Promises/A+ 來規範一下咱們的 Promise

 

規範 Promise

前幾版的代碼筆者基本上是按照規範來的,這裏主要講幾個沒有符合規範的點。

規範 then(規範 2.2)

thenonFulfilledonRejected 須要異步執行,即放到異步任務中去執行(規範 2.2.4)

實現

咱們須要將 then 中的函數經過 setTimeout 包裹起來,放到一個宏任務中去,這裏涉及了 jsEventLoop,你們能夠去看看相應的文章,以下:

class MyPromise {
  // ...

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

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

      // 等待狀態,將 onFulfilled,onRejected 塞入數組中,等待回調執行
      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 是屬於微任務的,如今當使用 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
複製代碼

輸出順序不同,緣由是由於如今的 Promise 是經過 setTimeout 宏任務包裹的。

咱們能夠改進一下,使用微任務來包裹 onFulfilledonRejected,經常使用的微任務有 process.nextTickMutationObserverpostMessage 等,咱們這個使用 postMessage 改寫一下:

// ...
if (this.status === MyPromise.RESOLVED) {
  // 將 onFulfilled 函數的返回值,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);
      }
    }
  });
  // 立馬執行
  window.postMessage({
    type: '__promise',
  }, "http://localhost:3001");
}

// ...
複製代碼

實現方法很簡單,咱們監聽windowmessage 事件,並在以後立馬觸發一個 postMessage 事件,這個時候其實 then 中的回調函數已經在微任務隊列中了,咱們從新運行一下例子,能夠看到輸出的順序變爲了 then -> setTimeout

固然 Promise 內部實現確定沒有這麼簡單,筆者在這裏只是提供一種思路,你們有興趣能夠去研究一波。

規範 resolvePromise 函數(規範 2.3)

重複引用

重複引用,當 xpromise2 是同樣的,那就須要報一個錯誤,重複應用。(規範 2.3.1)

由於本身等待本身完成是永遠都不會有結果的。

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

const p2 = p1.then((res) => {
  return p2;
});
複製代碼

x 的類型

大體分爲一下這麼幾條:

  • 2.3.2:當 x 是一個 Promise,那麼就等待 x 改變狀態以後,纔算完成或者失敗(這個也屬於 2.3.3,由於 Promise 其實也是一個 thenable 對象)
  • 2.3.3:當 x 是一個對象 或者 函數的時候,即 thenable 對象,那就那 x.then 做爲 then
  • 2.3.4:當 x 不是一個對象,或者函數的時候,直接將 x 做爲參數 resolve 返回。

咱們主要看一下 2.3.3 就行,由於 Prmise 也屬於 thenable 對象,那什麼是 thenable 對象呢?

簡單來講就是具備 then方法的對象/函數,全部的 Promise 對象都是 thenable 對象,但並不是全部的 thenable 對象並不是是 Promise 對象。以下:

let thenable = {
 then: function(resolve, reject) {
   resolve(100);
 }
}
複製代碼

根據 x 的類型進行處理:

  • 若是 x 不是 thenable 對象,直接調用 Promise2resolve,將 x 做爲成功的結果;

  • xthenable 對象,會調用 xthen 方法,成功後再去調用 resolvePromise 函數,並將執行結果 y 做爲新的 x 傳入 resolvePromise,直到這個 x 值再也不是一個 thenable 對象爲止;若是失敗則直接調用 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);
}
複製代碼

只調用一次

規範(Promise/A+ 2.3.3.3.3)規定若是同時調用 resolvePromiserejectPromise,或者對同一參數進行了屢次調用,則第一個調用優先,而全部其餘調用均被忽略,確保只執行一次改變狀態。

咱們在外面定義了一個 called 佔位符,爲了得到 then 函數有沒有執行過相應的改變狀態的函數,執行過了以後,就再也不去執行了,主要就是爲了知足規範。

x 爲 Promise 對象

若是 xPromise 對象的話,其實當執行了resolve 函數 以後,就不會再執行 reject 函數了,是直接在當前這個 Promise 對象就結束掉了。

x 爲 thenable 對象

x 是普通的 thenable 函數的時候,他就有可能同時執行 resolvereject 函數,便可以同時執行 promise2resolve 函數 和 reject 函數,可是其實 promise2 在狀態改變了以後,也不會再改變相應的值了。其實也沒有什麼問題,以下代碼:

// thenable 對像
{
 then: function(resolve, reject) {
   setTimeout(() => {
     resolve('我是thenable對像的 resolve');
     reject('我是thenable對像的 reject')
    })
 }
}
複製代碼

完整的 resolvePromise

完整的 resolvePromise 函數以下:

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);
  }
}
複製代碼

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

最後咱們能夠經過測試腳本跑一下咱們的 MyPromise 是否符合規範。

測試

有專門的測試腳本(promises-aplus-tests)能夠幫助咱們測試所編寫的代碼是否符合 Promise/A+ 的規範。

可是貌似只能測試 js 文件,因此筆者就將 ts 文件轉化爲了 js 文件,進行測試

在代碼裏面加上:

// 執行測試用例須要用到的代碼
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
複製代碼

結果以下:

完美經過,接下去咱們就能夠看看 Promise 更多方法的實現了。

 

更多方法

實現上面的 Promise 以後,其實編寫其實例和靜態方法,相對來講就簡單了不少。

實例方法

Promise.prototype.catch

實現

其實這個方法就是 then 方法的語法糖,只須要給 then 傳遞 onRejected 參數就 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

實現

finally() 方法用於指定無論 Promise 對象最後狀態如何,都會執行的操做。

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

// 輸出結果:---error 錯誤了" -> ""---finally--- catch-錯誤了
複製代碼

 

靜態方法

Promise.resolve

實現

有時須要將現有對象轉爲 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);
});

// 輸出結果:{name: "darrell", sex: "boy"}
複製代碼

Promise.reject

實現

Promise.reject(reason) 方法也會返回一個新的 Promise 實例,該實例的狀態爲 rejected

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

// 輸出結果:出錯了
複製代碼

Promise.all

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

const p = Promise.all([p1, p2, p3]);
複製代碼
  • 只有 p1p2p3 的狀態都變成 fulfilledp 的狀態纔會變成 fulfilled
  • 只要 p1p2p3 之中有一個被 rejectedp 的狀態就變成 rejected,此時第一個被 reject 的實例的返回值,會傳遞給p的回調函數。
實現
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]);
複製代碼

只要 p1p2p3 之中有一個實例率先改變狀態,p 的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調函數。

實現
static race = (promises) => {
  return new Promise((resolve,reject)=>{
    for(let i = 0; i < promises.length; i++){
      promises[i].then(resolve,reject)
    };
  })
}
複製代碼
例子

例子和 all 同樣,調用以下:

// ...

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

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

// 直接輸出:---成功了 Promise2
複製代碼

Promise.allSettled

此方法接受一組 Promise 實例做爲參數,包裝成一個新的 Promise 實例。

const p = Promise.race([p1, p2, p3]);
複製代碼

只有等到全部這些參數實例都返回結果,不論是 fulfilled 仍是 rejected,並且該方法的狀態只可能變成 fulfilled

此方法與 Promise.all 的區別是 all 沒法肯定全部請求都結束,由於在 all 中,若是有一個被 Promiserejectedp 的狀態就立馬變成 rejected,有可能有些異步請求還沒走完。

實現
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 同樣,調用以下:

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

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

// 2s 後輸出:---成功了 (4) ["Promise1", "Promise2", "Promise3", "Promise4"]
複製代碼

 

總結

這篇文章筆者帶你們一步一步的實現了符合 Promise/A+ 規範的的 Promise,看完以後相信你們基本上也可以本身獨立寫出一個 Promise 來了。

最後經過幾個問題,你們能夠看看本身掌握的如何:

  • Promise 中是如何實現回調函數返回值穿透的?
  • Promise 出錯後,是怎麼經過 冒泡 傳遞給最後那個捕獲異常的函數?
  • Promise 如何支持鏈式調用?
  • 怎麼將 Promise.then 包裝成一個微任務?

實不相瞞,想要個贊!

 

參考文檔

 

示例代碼

示例代碼能夠看這裏:

相關文章
相關標籤/搜索