從一道讓我失眠的 Promise 面試題開始,深入分析 Promise 實現細節

先把罪魁禍首掛在這裏給大家羣毆

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})
Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})
// 大家先思考一下

這道面試題是無意間在微信羣裏看到的,據說是某廠的面試題。一般關於 Promise 的面試題無非是考察宏微任務、EventLoop 之類的,當我認真去分析這道題的時候,越看越不對勁,感覺有詐!這是要考察啥?

不管了,先在瀏覽器輸出一下看看

打印結果:

0123456

這裏 4 怎麼跑到 3 後面去了,不講武德?Why......

在我看來,這道題有兩個 Promise.resolve(),相當於創建兩個狀態爲 fulfilled 的 Promise

如果說需要等待 return Promise.resolve(4) 執行完並將其結果和狀態同步給外部的 Promise,那麼這裏只需要創建一個微任務去處理就應該可以了,也就是 4 會在 2 後面纔對,爲啥需要創建兩個微任務呢?🤔

想了很久,也找很多朋友討論這個問題,都沒有得到有說服力的結論,真是百思不得其解!這樣死摳細節,感覺有點浪費時間,畢竟這種面試題在生產中並不會出現,誰會去寫這麼奇葩的 Promise 代碼, 放棄了,不去想了。

然而😂,當天晚上夜黑風高夜深人靜的時候,腦海裏面依然輪播這道面試題,真的很想知道 Promise 內部到底是個什麼邏輯,越想越睡不着~越睡不着越想~

無奈之下,決定參考 Promise A+ 規範手寫一版 Promise,看看能不能從實現細節中找到蛛絲馬跡。爲了方便大家理解,下面我會利用不同 🌰 來介紹手寫的細節和思路。文章最後會依據實現細節來探討這道面試題,有手寫經驗的可以直接跳過手寫 Promise 實現過程,看最後的結論。

如果感覺對 Promise 還不太熟悉的就先移步 Promise 入門,稍微做一下知識預習,瞭解一下 Promise 的常規用法。

什麼是宏任務與微任務?

我們都知道 Js 是單線程都,但是一些高耗時操作就帶來了進程阻塞問題。爲了解決這個問題,Js 有兩種任務的執行模式:同步模式(Synchronous)和異步模式(Asynchronous)

在異步模式下,創建異步任務主要分爲宏任務與微任務兩種。ES6 規範中,宏任務(Macrotask) 稱爲 Task, 微任務(Microtask) 稱爲 Jobs。宏任務是由宿主(瀏覽器、Node)發起的,而微任務由 JS 自身發起。

宏任務與微任務的幾種創建方式 👇

UkGCQL

如何理解 script(整體代碼塊)是個宏任務呢 🤔

實際上如果同時存在兩個 script 代碼塊,會首先在執行第一個 script 代碼塊中的同步代碼,如果這個過程中創建了微任務並進入了微任務隊列,第一個 script 同步代碼執行完之後,會首先去清空微任務隊列,再去開啓第二個 script 代碼塊的執行。所以這裏應該就可以理解 script(整體代碼塊)爲什麼會是宏任務。

什麼是 EventLoop ?

先來看個圖

  1. 判斷宏任務隊列是否爲空
  1. 判斷微任務隊列是否爲空

因爲首次執行宏隊列中會有 script(整體代碼塊)任務,所以實際上就是 Js 解析完成後,在異步任務中,會先執行完所有的微任務,這裏也是很多面試題喜歡考察的。需要注意的是,新創建的微任務會立即進入微任務隊列排隊執行,不需要等待下一次輪迴。

什麼是 Promise A+ 規範?

看到 A+ 肯定會想到是不是還有 A,事實上確實有。其實 Promise 有多種規範,除了前面的 Promise A、promise A+ 還有 Promise/B,Promise/D。目前我們使用的 Promise 是基於 Promise A+ 規範實現的,感興趣的移步 Promise A + 規範瞭解一下,這裏不贅述。

檢驗一份手寫 Promise 靠不靠譜,通過 Promise A+ 規範自然是基本要求,這裏我們可以藉助 promises-aplus-tests 來檢測我們的代碼是否符合規範,後面我會講到如何使用它。

手寫開始

這裏我們有幾種選擇,一種就是 Promise A+ 規範中也提到的,process.nextTick( Node 端 ) 與 MutationObserver( 瀏覽器端 ),考慮到利用這兩種方式需要做環境判斷,所以在這裏我們就推薦另外一種創建微任務的方式 queueMicrotask,瞭解更多 --> 在 JavaScript 中通過 queueMicrotask() 使用微任務;

一、Promise 核心邏輯實現

我們先簡單實現一下 Promise 的基礎功能。先看原生 Promise 實現的 🌰,第一步我們要完成相同的功能。

下面開始實現

1. 新建 MyPromise 類,傳入執行器 executor

// 新建 MyPromise.js
// 新建 MyPromise 類
class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    executor() 
  }
}

2. executor 傳入 resolve 和 reject 方法

// MyPromise.js
// 新建 MyPromise 類
class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    // 並傳入resolve和reject方法
    executor(this.resolve, this.reject) 
  }
  // resolve和reject爲什麼要用箭頭函數?
  // 如果直接調用的話,普通函數this指向的是window或者undefined
  // 用箭頭函數就可以讓this指向當前實例對象
  // 更改成功後的狀態
  resolve = () => {}
  // 更改失敗後的狀態
  reject = () => {}
}

3. 狀態與結果的管理

// MyPromise.js
// 先定義三個常量表示狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
// 新建 MyPromise 類
class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    // 並傳入resolve和reject方法
    executor(this.resolve, this.reject)
  }
  // 儲存狀態的變量,初始值是 pending
  status = PENDING;
  // resolve和reject爲什麼要用箭頭函數?
  // 如果直接調用的話,普通函數this指向的是window或者undefined
  // 用箭頭函數就可以讓this指向當前實例對象
  // 成功之後的值
  value = null;
  // 失敗之後的原因
  reason = null;
  // 更改成功後的狀態
  resolve = (value) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態修改爲成功
      this.status = FULFILLED;
      // 保存成功之後的值
      this.value = value;
    }
  }
  // 更改失敗後的狀態
  reject = (reason) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態成功爲失敗
      this.status = REJECTED;
      // 保存失敗後的原因
      this.reason = reason;
    }
  }
}

4. then 的簡單實現

// MyPromise.js
then(onFulfilled, onRejected) {
  // 判斷狀態
  if (this.status === FULFILLED) {
    // 調用成功回調,並且把值返回
    onFulfilled(this.value);
  } else if (this.status === REJECTED) {
    // 調用失敗回調,並且把原因返回
    onRejected(this.reason);
  }
}

5. 使用 module.exports 對外暴露 MyPromise 類

// MyPromise.js
module.exports = MyPromise;

看一下我們目前實現的完整代碼🥳

// MyPromise.js
// 先定義三個常量表示狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
// 新建 MyPromise 類
class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    // 並傳入resolve和reject方法
    executor(this.resolve, this.reject)
  }
  // 儲存狀態的變量,初始值是 pending
  status = PENDING;
  // resolve和reject爲什麼要用箭頭函數?
  // 如果直接調用的話,普通函數this指向的是window或者undefined
  // 用箭頭函數就可以讓this指向當前實例對象
  // 成功之後的值
  value = null;
  // 失敗之後的原因
  reason = null;
  // 更改成功後的狀態
  resolve = (value) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態修改爲成功
      this.status = FULFILLED;
      // 保存成功之後的值
      this.value = value;
    }
  }
  // 更改失敗後的狀態
  reject = (reason) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態成功爲失敗
      this.status = REJECTED;
      // 保存失敗後的原因
      this.reason = reason;
    }
  }
  then(onFulfilled, onRejected) {
    // 判斷狀態
    if (this.status === FULFILLED) {
      // 調用成功回調,並且把值返回
      onFulfilled(this.value);
    } else if (this.status === REJECTED) {
      // 調用失敗回調,並且把原因返回
      onRejected(this.reason);
    }
  }
}
module.exports = MyPromise

使用我的手寫代碼執行一下上面那個🌰

// 新建 test.js
// 引入我們的 MyPromise.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
   resolve('success')
   reject('err')
})
promise.then(value => {
  console.log('resolve', value)
}, reason => {
  console.log('reject', reason)
})
// 執行結果:resolve success

執行結果符合我們的預期,第一步完成了👏👏👏

二、在 Promise 類中加入異步邏輯

上面還沒有經過異步處理,如果有異步邏輯加如來會帶來一些問題,例如:

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 2000); 
})
promise.then(value => {
  console.log('resolve', value)
}, reason => {
  console.log('reject', reason)
})
// 沒有打印信息!!!

分析原因

主線程代碼立即執行,setTimeout 是異步代碼,then 會馬上執行,這個時候判斷 Promise 狀態,狀態是 Pending,然而之前並沒有判斷等待這個狀態

這裏就需要我們處理一下 Pending 狀態,我們改造一下之前的代碼 🤔

1、緩存成功與失敗回調

// MyPromise.js
// MyPromise 類中新增
// 存儲成功回調函數
onFulfilledCallback = null;
// 存儲失敗回調函數
onRejectedCallback = null;

2、then 方法中的 Pending 的處理

// MyPromise.js
then(onFulfilled, onRejected) {
  // 判斷狀態
  if (this.status === FULFILLED) {
    // 調用成功回調,並且把值返回
    onFulfilled(this.value);
  } else if (this.status === REJECTED) {
    // 調用失敗回調,並且把原因返回
    onRejected(this.reason);
  } else if (this.status === PENDING) {
    // ==== 新增 ====
    // 因爲不知道後面狀態的變化情況,所以將成功回調和失敗回調存儲起來
    // 等到執行成功失敗函數的時候再傳遞
    this.onFulfilledCallback = onFulfilled;
    this.onRejectedCallback = onRejected;
  }
}

3. resolve 與 reject 中調用回調函數

// MyPromise.js
// 更改成功後的狀態
resolve = (value) => {
  // 只有狀態是等待,才執行狀態修改
  if (this.status === PENDING) {
    // 狀態修改爲成功
    this.status = FULFILLED;
    // 保存成功之後的值
    this.value = value;
    // ==== 新增 ====
    // 判斷成功回調是否存在,如果存在就調用
    this.onFulfilledCallback && this.onFulfilledCallback(value);
  }
}
// MyPromise.js
// 更改失敗後的狀態
reject = (reason) => {
  // 只有狀態是等待,才執行狀態修改
  if (this.status === PENDING) {
    // 狀態成功爲失敗
    this.status = REJECTED;
    // 保存失敗後的原因
    this.reason = reason;
    // ==== 新增 ====
    // 判斷失敗回調是否存在,如果存在就調用
    this.onRejectedCallback && this.onRejectedCallback(reason)
  }
}

我們再執行一下上面的🌰

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 2000); 
})
promise.then(value => {
  console.log('resolve', value)
}, reason => {
  console.log('reject', reason)
})
// 等待 2s 輸出 resolve success

目前已經可以簡單處理異步問題了✌️

三、實現 then 方法多次調用添加多個處理函數

Promise 的 then 方法是可以被多次調用的。這裏如果有三個 then 的調用,如果是同步回調,那麼直接返回當前的值就行;如果是異步回調,那麼保存的成功失敗的回調,需要用不同的值保存,因爲都互不相同。之前的代碼需要改進。

同樣的先看一個🌰

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
  }, 2000); 
})
promise.then(value => {
  console.log(1)
  console.log('resolve', value)
})
promise.then(value => {
  console.log(2)
  console.log('resolve', value)
})
promise.then(value => {
  console.log(3)
  console.log('resolve', value)
})
// 3
// resolve success

目前的代碼只能輸出:3 resolve success,怎麼可以把 1、2 弄丟呢!

我們應該一視同仁,保證所有 then 中的回調函數都可以執行 🤔 繼續改造

1. MyPromise 類中新增兩個數組

// MyPromise.js
// 存儲成功回調函數
// onFulfilledCallback = null;
onFulfilledCallbacks = [];
// 存儲失敗回調函數
// onRejectedCallback = null;
onRejectedCallbacks = [];

2. 回調函數存入數組中

// MyPromise.js
then(onFulfilled, onRejected) {
  // 判斷狀態
  if (this.status === FULFILLED) {
    // 調用成功回調,並且把值返回
    onFulfilled(this.value);
  } else if (this.status === REJECTED) {
    // 調用失敗回調,並且把原因返回
    onRejected(this.reason);
  } else if (this.status === PENDING) {
    // ==== 新增 ====
    // 因爲不知道後面狀態的變化,這裏先將成功回調和失敗回調存儲起來
    // 等待後續調用
    this.onFulfilledCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
  }
}

3. 循環調用成功和失敗回調

// MyPromise.js
// 更改成功後的狀態
resolve = (value) => {
  // 只有狀態是等待,才執行狀態修改
  if (this.status === PENDING) {
    // 狀態修改爲成功
    this.status = FULFILLED;
    // 保存成功之後的值
    this.value = value;
    // ==== 新增 ====
    // resolve裏面將所有成功的回調拿出來執行
    while (this.onFulfilledCallbacks.length) {
      // Array.shift() 取出數組第一個元素,然後()調用,shift不是純函數,取出後,數組將失去該元素,直到數組爲空
      this.onFulfilledCallbacks.shift()(value)
    }
  }
}
// MyPromise.js
// 更改失敗後的狀態
reject = (reason) => {
  // 只有狀態是等待,才執行狀態修改
  if (this.status === PENDING) {
    // 狀態成功爲失敗
    this.status = REJECTED;
    // 保存失敗後的原因
    this.reason = reason;
    // ==== 新增 ====
    // resolve裏面將所有失敗的回調拿出來執行
    while (this.onRejectedCallbacks.length) {
      this.onRejectedCallbacks.shift()(reason)
    }
  }
}

```js

再來運行一下,看看結果👇

1 resolve success 2 resolve success 3 resolve success


👏👏👏 完美,繼續

### 四、實現 then 方法的鏈式調用

then 方法要鏈式調用那麼就需要返回一個 Promise 對象  
then 方法裏面 return 一個返回值作爲下一個 then 方法的參數,如果是 return 一個 Promise 對象,那麼就需要判斷它的狀態

舉個栗子 🌰

```js
// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  // 目前這裏只處理同步的問題
  resolve('success')
})
function other () {
  return new MyPromise((resolve, reject) =>{
    resolve('other')
  })
}
promise.then(value => {
  console.log(1)
  console.log('resolve', value)
  return other()
}).then(value => {
  console.log(2)
  console.log('resolve', value)
})

用目前的手寫代碼運行的時候會報錯 😣 無法鏈式調用

}).then(value => {
  ^
TypeError: Cannot read property 'then' of undefined

接着改 💪

// MyPromise.js
class MyPromise {
  ......
  then(onFulfilled, onRejected) {
    // ==== 新增 ====
    // 爲了鏈式調用這裏直接創建一個 MyPromise,並在後面 return 出去
    const promise2 = new MyPromise((resolve, reject) => {
      // 這裏的內容在執行器中,會立即執行
      if (this.status === FULFILLED) {
        // 獲取成功回調函數的執行結果
        const x = onFulfilled(this.value);
        // 傳入 resolvePromise 集中處理
        resolvePromise(x, resolve, reject);
      } else if (this.status === REJECTED) {
        onRejected(this.reason);
      } else if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
      }
    }) 
    return promise2;
  }
}
function resolvePromise(x, resolve, reject) {
  // 判斷x是不是 MyPromise 實例對象
  if(x instanceof MyPromise) {
    // 執行 x,調用 then 方法,目的是將其狀態變爲 fulfilled 或者 rejected
    // x.then(value => resolve(value), reason => reject(reason))
    // 簡化之後
    x.then(resolve, reject)
  } else{
    // 普通值
    resolve(x)
  }
}

執行一下,結果👇

1
resolve success
2
resolve other

em... 符合預期 😎

五、then 方法鏈式調用識別 Promise 是否返回自己

如果 then 方法返回的是自己的 Promise 對象,則會發生循環調用,這個時候程序會報錯

例如下面這種情況👇

// test.js
const promise = new Promise((resolve, reject) => {
  resolve(100)
})
const p1 = promise.then(value => {
  console.log(value)
  return p1
})

使用原生 Promise 執行這個代碼,會報類型錯誤

100
Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>

我們在 MyPromise 實現一下

// MyPromise.js
class MyPromise {
  ......
  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        const x = onFulfilled(this.value);
        // resolvePromise 集中處理,將 promise2 傳入
        resolvePromise(promise2, x, resolve, reject);
      } else if (this.status === REJECTED) {
        onRejected(this.reason);
      } else if (this.status === PENDING) {
        this.onFulfilledCallbacks.push(onFulfilled);
        this.onRejectedCallbacks.push(onRejected);
      }
    }) 
    return promise2;
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  // 如果相等了,說明return的是自己,拋出類型錯誤並返回
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  if(x instanceof MyPromise) {
    x.then(resolve, reject)
  } else{
    resolve(x)
  }
}

執行一下,竟然報錯了 😱

resolvePromise(promise2, x, resolve, reject);
                       ^
ReferenceError: Cannot access 'promise2' before initialization

爲啥會報錯呢?從錯誤提示可以看出,我們必須要等 promise2 完成初始化。這個時候我們就要用上宏微任務和事件循環的知識了,這裏就需要創建一個異步函數去等待 promise2 完成初始化,前面我們已經確認了創建微任務的技術方案 --> queueMicrotask

// MyPromise.js
class MyPromise {
  ......
  then(onFulfilled, onRejected) {
    const promise2 = new MyPromise((resolve, reject) => {
      if (this.status === FULFILLED) {
        // ==== 新增 ====
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          // 獲取成功回調函數的執行結果
          const x = onFulfilled(this.value);
          // 傳入 resolvePromise 集中處理
          resolvePromise(promise2, x, resolve, reject);
        })  
      } else if (this.status === REJECTED) {
      ......
    }) 
    return promise2;
  }
}

執行一下

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
    resolve('success')
})
// 這個時候將promise定義一個p1,然後返回的時候返回p1這個promise
const p1 = promise.then(value => {
   console.log(1)
   console.log('resolve', value)
   return p1
})
// 運行的時候會走reject
p1.then(value => {
  console.log(2)
  console.log('resolve', value)
}, reason => {
  console.log(3)
  console.log(reason.message)
})

這裏得到我們的結果 👇

1
resolve success
3
Chaining cycle detected for promise #<Promise>

哈哈,搞定 😎 開始下一步

目前還缺少重要的一個環節,就是我們的錯誤捕獲還沒有處理

1. 捕獲執行器錯誤

捕獲執行器中的代碼,如果執行器中有代碼錯誤,那麼 Promise 的狀態要變爲失敗

// MyPromise.js
constructor(executor){
  // ==== 新增 ====
  // executor 是一個執行器,進入會立即執行
  // 並傳入resolve和reject方法
  try {
    executor(this.resolve, this.reject)
  } catch (error) {
    // 如果有錯誤,就直接執行 reject
    this.reject(error)
  }
}

驗證一下:

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
    // resolve('success')
    throw new Error('執行器錯誤')
})
promise.then(value => {
  console.log(1)
  console.log('resolve', value)
}, reason => {
  console.log(2)
  console.log(reason.message)
})

執行結果 👇

2
執行器錯誤

OK,通過 😀

2. then 執行的時錯誤捕獲

// MyPromise.js
then(onFulfilled, onRejected) {
  // 爲了鏈式調用這裏直接創建一個 MyPromise,並在後面 return 出去
  const promise2 = new MyPromise((resolve, reject) => {
    // 判斷狀態
    if (this.status === FULFILLED) {
      // 創建一個微任務等待 promise2 完成初始化
      queueMicrotask(() => {
        // ==== 新增 ====
        try {
          // 獲取成功回調函數的執行結果
          const x = onFulfilled(this.value);
          // 傳入 resolvePromise 集中處理
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error)
        }  
      })  
    } else if (this.status === REJECTED) {
      // 調用失敗回調,並且把原因返回
      onRejected(this.reason);
    } else if (this.status === PENDING) {
      // 等待
      // 因爲不知道後面狀態的變化情況,所以將成功回調和失敗回調存儲起來
      // 等到執行成功失敗函數的時候再傳遞
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected);
    }
  }) 
  return promise2;
}

驗證一下:

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
    resolve('success')
    // throw new Error('執行器錯誤')
 })
// 第一個then方法中的錯誤要在第二個then方法中捕獲到
promise.then(value => {
  console.log(1)
  console.log('resolve', value)
  throw new Error('then error')
}, reason => {
  console.log(2)
  console.log(reason.message)
}).then(value => {
  console.log(3)
  console.log(value);
}, reason => {
  console.log(4)
  console.log(reason.message)
})

執行結果 👇

1
resolve success
4
then error

這裏成功打印了 1 中拋出的錯誤 then error

七、參考 fulfilled 狀態下的處理方式,對 rejected 和 pending 狀態進行改造

改造內容包括:

  1. 增加異步狀態下的鏈式調用

  2. 增加回調函數執行結果的判斷

  3. 增加識別 Promise 是否返回自己

  4. 增加錯誤捕獲

// MyPromise.js
then(onFulfilled, onRejected) {
  // 爲了鏈式調用這裏直接創建一個 MyPromise,並在後面 return 出去
  const promise2 = new MyPromise((resolve, reject) => {
    // 判斷狀態
    if (this.status === FULFILLED) {
      // 創建一個微任務等待 promise2 完成初始化
      queueMicrotask(() => {
        try {
          // 獲取成功回調函數的執行結果
          const x = onFulfilled(this.value);
          // 傳入 resolvePromise 集中處理
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error)
        } 
      })  
    } else if (this.status === REJECTED) { 
      // ==== 新增 ====
      // 創建一個微任務等待 promise2 完成初始化
      queueMicrotask(() => {
        try {
          // 調用失敗回調,並且把原因返回
          const x = onRejected(this.reason);
          // 傳入 resolvePromise 集中處理
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          reject(error)
        } 
      }) 
    } else if (this.status === PENDING) {
      // 等待
      // 因爲不知道後面狀態的變化情況,所以將成功回調和失敗回調存儲起來
      // 等到執行成功失敗函數的時候再傳遞
      this.onFulfilledCallbacks.push(() => {
        // ==== 新增 ====
        queueMicrotask(() => {
          try {
            // 獲取成功回調函數的執行結果
            const x = onFulfilled(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        }) 
      });
      this.onRejectedCallbacks.push(() => {
        // ==== 新增 ====
        queueMicrotask(() => {
          try {
            // 調用失敗回調,並且把原因返回
            const x = onRejected(this.reason);
            // 傳入 resolvePromise 集中處理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        }) 
      });
    }
  }) 
  return promise2;
}

八、then 中的參數變爲可選

上面我們處理 then 方法的時候都是默認傳入 onFulfilled、onRejected 兩個回調函數,但是實際上原生 Promise 是可以選擇參數的單傳或者不傳,都不會影響執行。

例如下面這種 👇

// test.js
const promise = new Promise((resolve, reject) => {
  resolve(100)
})
promise
  .then()
  .then()
  .then()
  .then(value => console.log(value))
// 輸出 100

所以我們需要對 then 方法做一點小小的調整

// MyPromise.js
then(onFulfilled, onRejected) {
  // 如果不傳,就使用默認函數
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};
  // 爲了鏈式調用這裏直接創建一個 MyPromise,並在後面 return 出去
  const promise2 = new MyPromise((resolve, reject) => {
  ......
}

改造完自然是需要驗證一下的

先看情況一:resolve 之後

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  resolve('succ')
})
promise.then().then().then(value => console.log(value))
// 打印 succ

先看情況一:reject 之後

// test.js
const MyPromise = require('./MyPromise')
const promise = new MyPromise((resolve, reject) => {
  reject('err')
})
promise.then().then().then(value => console.log(value), reason => console.log(reason))
// 打印 err

寫到這裏,麻雀版的 Promise 基本完成了,鼓掌 👏👏👏

九、實現 resolve 與 reject 的靜態調用

就像開頭掛的那道面試題使用 return Promise.resolve 來返回一個 Promise 對象,我們用現在的手寫代碼嘗試一下

const MyPromise = require('./MyPromise')
MyPromise.resolve().then(() => {
    console.log(0);
    return MyPromise.resolve(4);
}).then((res) => {
    console.log(res)
})

結果它報錯了 😥

MyPromise.resolve().then(() => {
          ^
TypeError: MyPromise.resolve is not a function

除了 Promise.resolve 還有 Promise.reject 的用法,我們都要去支持,接下來我們來實現一下

// MyPromise.js
MyPromise {
  ......
  // resolve 靜態方法
  static resolve (parameter) {
    // 如果傳入 MyPromise 就直接返回
    if (parameter instanceof MyPromise) {
      return parameter;
    }
    // 轉成常規方式
    return new MyPromise(resolve =>  {
      resolve(parameter);
    });
  }
  // reject 靜態方法
  static reject (reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }
}

這樣我們再測試上面的 🌰 就不會有問題啦

執行結果 👇

0
4

到這裏手寫工作就基本完成了,前面主要爲了方便理解,所以有一些冗餘代碼,我規整一下

// MyPromise.js
// 先定義三個常量表示狀態
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
// 新建 MyPromise 類
class MyPromise {
  constructor(executor){
    // executor 是一個執行器,進入會立即執行
    // 並傳入resolve和reject方法
    try {
      executor(this.resolve, this.reject)
    } catch (error) {
      this.reject(error)
    }
  }
  // 儲存狀態的變量,初始值是 pending
  status = PENDING;
  // 成功之後的值
  value = null;
  // 失敗之後的原因
  reason = null;
  // 存儲成功回調函數
  onFulfilledCallbacks = [];
  // 存儲失敗回調函數
  onRejectedCallbacks = [];
  // 更改成功後的狀態
  resolve = (value) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態修改爲成功
      this.status = FULFILLED;
      // 保存成功之後的值
      this.value = value;
      // resolve裏面將所有成功的回調拿出來執行
      while (this.onFulfilledCallbacks.length) {
        // Array.shift() 取出數組第一個元素,然後()調用,shift不是純函數,取出後,數組將失去該元素,直到數組爲空
        this.onFulfilledCallbacks.shift()(value)
      }
    }
  }
  // 更改失敗後的狀態
  reject = (reason) => {
    // 只有狀態是等待,才執行狀態修改
    if (this.status === PENDING) {
      // 狀態成功爲失敗
      this.status = REJECTED;
      // 保存失敗後的原因
      this.reason = reason;
      // resolve裏面將所有失敗的回調拿出來執行
      while (this.onRejectedCallbacks.length) {
        this.onRejectedCallbacks.shift()(reason)
      }
    }
  }
  then(onFulfilled, onRejected) {
    const realOnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    const realOnRejected = typeof onRejected === 'function' ? onRejected : reason => {throw reason};
    // 爲了鏈式調用這裏直接創建一個 MyPromise,並在後面 return 出去
    const promise2 = new MyPromise((resolve, reject) => {
      const fulfilledMicrotask = () =>  {
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 獲取成功回調函數的執行結果
            const x = realOnFulfilled(this.value);
            // 傳入 resolvePromise 集中處理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        })  
      }
      const rejectedMicrotask = () => { 
        // 創建一個微任務等待 promise2 完成初始化
        queueMicrotask(() => {
          try {
            // 調用失敗回調,並且把原因返回
            const x = realOnRejected(this.reason);
            // 傳入 resolvePromise 集中處理
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            reject(error)
          } 
        }) 
      }
      // 判斷狀態
      if (this.status === FULFILLED) {
        fulfilledMicrotask() 
      } else if (this.status === REJECTED) { 
        rejectedMicrotask()
      } else if (this.status === PENDING) {
        // 等待
        // 因爲不知道後面狀態的變化情況,所以將成功回調和失敗回調存儲起來
        // 等到執行成功失敗函數的時候再傳遞
        this.onFulfilledCallbacks.push(fulfilledMicrotask);
        this.onRejectedCallbacks.push(rejectedMicrotask);
      }
    }) 
    return promise2;
  }
  // resolve 靜態方法
  static resolve (parameter) {
    // 如果傳入 MyPromise 就直接返回
    if (parameter instanceof MyPromise) {
      return parameter;
    }
    // 轉成常規方式
    return new MyPromise(resolve =>  {
      resolve(parameter);
    });
  }
  // reject 靜態方法
  static reject (reason) {
    return new MyPromise((resolve, reject) => {
      reject(reason);
    });
  }
}
function resolvePromise(promise2, x, resolve, reject) {
  // 如果相等了,說明return的是自己,拋出類型錯誤並返回
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))
  }
  // 判斷x是不是 MyPromise 實例對象
  if(x instanceof MyPromise) {
    // 執行 x,調用 then 方法,目的是將其狀態變爲 fulfilled 或者 rejected
    // x.then(value => resolve(value), reason => reject(reason))
    // 簡化之後
    x.then(resolve, reject)
  } else{
    // 普通值
    resolve(x)
  }
}
module.exports = MyPromise;

到這一步手寫部分基本大功告成 🎉🎉🎉

Promise A+ 測試

上面介紹了 Promise A+ 規範,當然我們手寫的版本也得符合了這個規範纔有資格叫 Promise, 不然就只能是僞 Promise 了。

上文講到了 promises-aplus-tests,現在我們正式開箱使用

1. 安裝一下

npm install promises-aplus-tests -D

2. 手寫代碼中加入 deferred

// MyPromise.js
MyPromise {
  ......
}
MyPromise.deferred = function () {
  var result = {};
  result.promise = new MyPromise(function (resolve, reject) {
    result.resolve = resolve;
    result.reject = reject;
  });
  return result;
}
module.exports = MyPromise;

3. 配置啓動命令

{
  "name": "promise",
  "version": "1.0.0",
  "description": "my promise",
  "main": "MyPromise.js",
  "scripts": {
    "test": "promises-aplus-tests MyPromise"
  },
  "author": "ITEM",
  "license": "ISC",
  "devDependencies": {
    "promises-aplus-tests": "^2.1.2"
  }
}

開啓測試

npm run test

測試結果上雖然功能上沒啥問題,但是測試卻失敗了 😥

針對提示信息,我翻看了一下 Promise A+ 規範,發現我們應該是在 2.3.x 上出現了問題,這裏規範使用了不同的方式進行了 then 的返回值判斷。

自紅線向下的細節,我們都沒有處理,這裏要求判斷 x 是否爲 object 或者 function,滿足則接着判斷 x.then 是否存在,這裏可以理解爲判斷 x 是否爲 promise,這裏都功能實際與我們手寫版本中 x instanceof MyPromise 功能相似。

我們還是按照規範改造一下 resolvePromise 方法吧。

// MyPromise.js
function resolvePromise(promise, x, resolve, reject) {
  // 如果相等了,說明return的是自己,拋出類型錯誤並返回
  if (promise === x) {
    return reject(new TypeError('The promise and the return value are the same'));
  }
  if (typeof x === 'object' || typeof x === 'function') {
    // x 爲 null 直接返回,走後面的邏輯會報錯
    if (x === null) {
      return resolve(x);
    }
    let then;
    try {
      // 把 x.then 賦值給 then 
      then = x.then;
    } catch (error) {
      // 如果取 x.then 的值時拋出錯誤 error ,則以 error 爲據因拒絕 promise
      return reject(error);
    }
    // 如果 then 是函數
    if (typeof then === 'function') {
      let called = false;
      try {
        then.call(
          x, // this 指向 x
          // 如果 resolvePromise 以值 y 爲參數被調用,則運行 [[Resolve]](promise, y)
          y => {
            // 如果 resolvePromise 和 rejectPromise 均被調用,
            // 或者被同一參數調用了多次,則優先採用首次調用並忽略剩下的調用
            // 實現這條需要前面加一個變量 called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以據因 r 爲參數被調用,則以據因 r 拒絕 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          });
      } catch (error) {
        // 如果調用 then 方法拋出了異常 error:
        // 如果 resolvePromise 或 rejectPromise 已經被調用,直接返回
        if (called) return;
        // 否則以 error 爲據因拒絕 promise
        reject(error);
      }
    } else {
      // 如果 then 不是函數,以 x 爲參數執行 promise
      resolve(x);
    }
  } else {
    // 如果 x 不爲對象或者函數,以 x 爲參數執行 promise
    resolve(x);
  }
}

改造後啓動測試

完美通過 👏👏👏

最終時刻,如何解釋那道面試題的執行結果

先用我們自己的 Promise 運行一下那道面試題 👇

// test.js
const MyPromise = require('./MyPromise.js')
MyPromise.resolve().then(() => {
  console.log(0);
  return MyPromise.resolve(4);
}).then((res) => {
  console.log(res)
})
MyPromise.resolve().then(() => {
  console.log(1);
}).then(() => {
  console.log(2);
}).then(() => {
  console.log(3);
}).then(() => {
  console.log(5);
}).then(() =>{
  console.log(6);
})

執行結果:0、1、2、4、3、5、6

這裏我們手寫版本的 4 並沒有和 原生 Promise 一樣在 3 後面,而是在 2 後面

其實從我們的手寫代碼上看,在判斷 then 內部函數執行結果,也就是在這裏 👇

// MyPromise.js
// 獲取成功回調函數的執行結果
const x = realOnFulfilled(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(promise2, x, resolve, reject);

面試題中 x 爲 MyPromise.resolve(4) 的時候,在傳入 resolvePromise 方法中會對 x 的類型進行判斷時,會發現它是一個 Promise,並讓其調用 then 方法完成狀態轉換。再看 resolvePromis 方法中這一塊判斷邏輯 👇

if (typeof x === 'object' || typeof x === 'function') {
    // x 爲 null 直接返回,走後面的邏輯會報錯
    if (x === null) {
      return resolve(x);
    }
    let then;
    try {
      // 把 x.then 賦值給 then 
      then = x.then;
    } catch (error) {
      // 如果取 x.then 的值時拋出錯誤 error ,則以 error 爲據因拒絕 promise
      return reject(error);
    }
    // 如果 then 是函數
    if (typeof then === 'function') {
      let called = false;
      try {
        then.call(
          x, // this 指向 x
          // 如果 resolvePromise 以值 y 爲參數被調用,則運行 [[Resolve]](promise, y)
          y => {
            // 如果 resolvePromise 和 rejectPromise 均被調用,
            // 或者被同一參數調用了多次,則優先採用首次調用並忽略剩下的調用
            // 實現這條需要前面加一個變量 called
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          // 如果 rejectPromise 以據因 r 爲參數被調用,則以據因 r 拒絕 promise
          r => {
            if (called) return;
            called = true;
            reject(r);
          });
      } 
      ......

那麼問題來了

ES6 中的 Promise 雖然是遵循 Promise A+ 規範實現的,但實際上也 Promise A+ 上做了一些功能擴展,例如:Promise.all、Promise.race  等,所以即使都符合 Promise A+ ,執行結果也是可能存在差異的。我們這裏更需要思考的是第二個問題,不這麼做會帶來什麼問題,也就是加一次微任務的必要性。

我嘗試過很多例子,都沒有找到相關例證,我們手寫實現的 Promise 都很好的完成工作,拿到了結果。我不得不去翻看更多的相關文章,我發現有些人會爲了讓執行結果與原生相同,強行去再多加一次微任務,這種做法是很牽強的。

畢竟實現 Promise 的目的是爲了解決異步編程的問題,能夠拿到正確的結果纔是最重要的,強行爲了符合面試題的輸出順序去多加一次微任務,只能讓手寫代碼變的更加複雜,不好理解。

在 stackoverflow 上,有一個類似的問題 What is the difference between returned Promise? 回答中有一個信息就是

It only required the execution context stack contains only platform code. 也就相當於等待 execution context stack 清空。

這個在掘金中的一篇文章 我以爲我很懂 Promise,直到我開始實現 Promise/A + 規範 也有一段關於這道面試題的討論

return Promise.resolve(4),JS 引擎會安排一個 job(job 是 ECMA 中的概念,等同於微任務的概念),其回調目的是讓其狀態變爲 fulfilled。

實際上我們已經在 static resolve 創建了一個新的 MyPromsie,並調用其 then 方法,創建了一個微任務。

所以,就目前的信息來說,兩次微任務依舊不能證明其必要性,目前的 Promise 日常操作,一次微任務都是可以滿足。

大家對於這個道面試題有什麼想法或者意見,趕緊在留言區告訴我吧,一起探討一下到底是必然還是巧合🤔

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/07MC3WQ6pbhXFSfA5QlCRA