從一道讓我失眠的 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 之類的,當我認真去分析這道題的時候,越看越不對勁,感覺有詐!這是要考察啥?
不管了,先在瀏覽器輸出一下看看
打印結果:
0、1、2、3、4、5、6
這裏 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 自身發起。
宏任務與微任務的幾種創建方式 👇
如何理解 script(整體代碼塊)是個宏任務呢 🤔
實際上如果同時存在兩個 script 代碼塊,會首先在執行第一個 script 代碼塊中的同步代碼,如果這個過程中創建了微任務並進入了微任務隊列,第一個 script 同步代碼執行完之後,會首先去清空微任務隊列,再去開啓第二個 script 代碼塊的執行。所以這裏應該就可以理解 script(整體代碼塊)爲什麼會是宏任務。
什麼是 EventLoop ?
先來看個圖
- 判斷宏任務隊列是否爲空
-
不空 --> 執行最早進入隊列的任務 --> 執行下一步
-
空 --> 執行下一步
- 判斷微任務隊列是否爲空
-
不空 --> 執行最早進入隊列的任務 --> 繼續檢查微任務隊列空不空
-
空 --> 執行下一步
因爲首次執行宏隊列中會有 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 實現的 🌰,第一步我們要完成相同的功能。
-
Pending 等待
-
Fulfilled 完成
-
Rejected 失敗
-
狀態只能由 Pending --> Fulfilled 或者 Pending --> Rejected,且一但發生改變便不可二次修改;
-
Promise 中使用 resolve 和 reject 兩個函數來更改狀態;
-
then 方法內部做但事情就是狀態判斷
-
如果狀態是成功,調用成功回調函數
-
如果狀態是失敗,調用失敗回調函數
下面開始實現:
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 狀態進行改造
改造內容包括:
-
增加異步狀態下的鏈式調用
-
增加回調函數執行結果的判斷
-
增加識別 Promise 是否返回自己
-
增加錯誤捕獲
// 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);
});
}
......
那麼問題來了
-
爲什麼我們 Promise A+ 測試全部通過的手寫代碼,執行結果卻與原生 Promise 不同?
-
在我們手寫代碼使用創建一次微任務的方式,會帶來什麼問題嗎?
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