
壹 ❀ 引
其實在去年七月份,博客所認識的一個朋友問了我一個關於Promise
執行先后的問題,具體代碼如下:
const fn = (s) => (
new Promise((resolve, reject) => {
if (typeof s === 'number') {
resolve();
} else {
reject();
}
})
.then(
res => console.log('參數是一個number'),
)
.catch(err => console.log('參數是一個字符串'))
)
fn('1');
fn(1);
// 先輸出 參數是一個number
// 后輸出 參數是一個字符串
他的疑惑是,以上代碼中關於Promise
狀態的修改都是同步的,那為什么fn(1)
的輸出還要早於fn('1')
?
說來慚愧,我當時對於這個輸出也疑惑了半天,最后基於自己掌握的現有知識,給了對方一個自認為說的過去但現在回想起來非常錯誤的解釋...想起來真是羞愧= =,這個問題也讓我當時有了了解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);
})
// 輸出為0 1 2 3 4 5 6
我看了一眼題,結果難道不應該是0 1 4 2 3 5 6
?對方抱着疑問而來,結果這次我自己都蒙圈了,這也讓我意識到自己對於Promise
的理解確實有點薄弱。
我承認,上面兩道題真的有點為考而考的意思了,畢竟實際開發我們也不可能寫出像例子2這樣的代碼,但站在面試的角度,對方總是需要一些評判標准來篩掉部分人,人人都不想卷,卻又不得不卷,多懂一點總是沒有壞處。
既然意識到自己的不足,那就花點功夫去了解Promise
原理,如何了解?當然是模擬實現一個Promise
,所以本篇文章的初衷是通過手寫Promise
的過程理解底層到底發生了什么,從而反向解釋上面兩道題為什么會這樣。放心吧,當我寫完我已經恍然大悟,所以你也一定可以,那么本文開始。
貳 ❀ 從零手寫Promise
貳 ❀ 壹 搭建框架
對於手寫新手而言,從零開始寫一個Promise
真正的難點在於你可能不清楚到底要實現Promise
哪些特性,沒事,我們從一個最簡單的例子開始分析:
const p = new Promise((resolve, reject) => {
// 同步執行
resolve(1);
});
p.then(
res => console.log(res),
err => console.log(err)
);
從上述代碼我們可以提煉出如下信息:
new
過程是同步的,我們傳遞了一個函數(resolve, reject)=>{resolve(1)}
給Promise
,它會幫我們同步執行這個函數。- 我們傳遞的函數接受
resolve reject
兩個參數,這兩個參數由Promise
提供,所以Promise
一定得有這兩個方法。 new Promise
返回了一個實例,這個實例能調用then
方法,因此Promise
內部一定得實現then
方法。
我們也別想那么多,先搭建一個基本的Promise
框架,代碼如下:
class MyPromise {
constructor(fn) {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
}
resolve = () => {}
reject = () => {}
then = () => {}
}
在constructor
中接受的參數fn
其實就是new Promise
傳遞的函數,我們在constructor
中同步調用它,同時傳遞了this.resolve
與this.reject
,這也就解釋了為何傳遞的函數會同步執行,以及如何使用到Promsise
提供的resolve
方法。
貳 ❀ 貳 增加狀態管理與值記錄
我們知道Promise
有pending、fuldilled、rejected
三種狀態,且狀態一旦改變就無法更改,無論成功失敗或者失敗,Promise
總是會返回一個succesValue
或者failReason
回去,所以我們來初始化狀態、value以及初步的成功/失敗邏輯:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(fn) {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
}
// 初始化狀態以及value
status = PENDING;
value = null;
resolve = (value) => {
// 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject = (reason) => {
// 當調用reject時修改狀態成rejected,同時記錄失敗的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
}
}
then = () => {}
}
叄 ❀ 叄 初步實現then
在實現Promise
狀態管理以及值記錄后,我們接着來看看then
,很明顯then
接受兩個參數,其實就是成功的與失敗的回調,而這兩個函數我們也得根據之前的this.status
來決定要不要執行,直接上代碼:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
class MyPromise {
constructor(fn) {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
}
// 初始化狀態以及成功,失敗的值
status = PENDING;
value = null;
resolve = (value) => {
// 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
}
}
reject = (reason) => {
// 當調用reject時修改狀態成rejected,同時記錄失敗的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
}
}
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn
};
callbackMap[this.status](this.value);
}
}
那么到這里我們已經實現了一個簡陋的MyPromise
,讓我們檢驗下狀態改變以及回調執行:
const p = new MyPromise((resolve, reject) => {
// 同步執行
resolve(1);
reject(2);
});
p.then(
res => console.log(res),
err => console.log(err)
);
// 只輸出了1
上述代碼只輸出了1,說明狀態控制以及回調處理都非常成功!!!我們繼續。
貳 ❀ 肆 異步修改狀態
上述代碼雖然運行正常,但其實只考慮了同步resolve
的情況,假設我們修改狀態在異步上下文中,就會引發意想不到的錯誤,比如:
const p = new MyPromise((resolve, reject) => {
// 同步執行
setTimeout(() => resolve(1), 2000);
});
p.then(
(res) => console.log(res),
(err) => console.log(err)
);
Uncaught TypeError: callbackMap[this.status] is not a function
簡單分析下,因為目前我們對於Promise
狀態的修改依賴了resolve
,但因為定時器的緣故,導致執行p.then
執行時狀態其實還是pending
,從而造成callbackMap[this.status]
無法匹配,因此我們需要添加一個pending
狀態的處理。
還有個問題,即使解決了callbackMap
匹配報錯,定時器等待結束后執行resolve
,我們怎么再次觸發對應回調的執行呢?要不我們在pending
狀態中把兩個回調記錄下來,然后在resolve
或者reject
時再調用記錄的回調?說干就干:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
class MyPromise {
constructor(fn) {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
}
// 初始化狀態以及成功,失敗的值
status = PENDING;
value = null;
// 新增記錄成功與失敗回調的參數
fulfilledCallback = null;
rejectedCallback = null;
resolve = (value) => {
// 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回調的調用
this.fulfilledCallback?.(value);
}
};
reject = (reason) => {
// 當調用reject時修改狀態成rejected,同時記錄失敗的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失敗回調的調用
this.rejectedCallback?.(reason);
}
};
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback = fulfilledFn;
this.rejectedCallback = rejectedFn;
},
};
callbackMap[this.status](this.value);
};
}
再次執行上面定時器的例子,現在不管有沒有異步修改狀態,都能正常執行了!!!
貳 ❀ 伍 實現then多次調用
當我們new
一個Promise
后會得到一個實例,而這個實例其實是支持多次then
調用的,比如:
const p = new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 0);
});
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
p.then((res) => console.log(res));// 1
但如果我們我們使用自己實現的MyPromise
去做相同的調用,你會發現只會輸出1個1,原因也很簡單,我們在pending
情況下記錄回調的邏輯只能記錄一個,所以還得再改造一下,將fulfilledCallback
定義成一個數組,如下:
class MyPromise {
// ....
// 修改為數組
fulfilledCallback = [];
rejectedCallback = [];
resolve = (value) => {
// 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回調的調用
while (this.fulfilledCallback.length) {
this.fulfilledCallback.shift()?.(value);
}
}
};
reject = (reason) => {
// 當調用reject時修改狀態成rejected,同時記錄失敗的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失敗回調的調用
while (this.rejectedCallback.length) {
this.rejectedCallback.shift()?.(reason);
}
}
};
then = (fulfilledFn, rejectedFn) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
callbackMap[this.status](this.value);
};
}
這也修改完成后再次執行上述例子,我們發現多次調用then
已滿足。
貳 ❀ 陸 實現then鏈式調用
OK,終於來到Promise
鏈式調用這個環節了,對於整個Promise
手寫,我個人覺得這部分是稍微有點繞,不過我會盡力解釋清楚,我們先看個最簡單的例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(2));
}).then((res) => {
console.log(res);
});
// 1
// 2
假設我們將上述代碼中的new Promise
都改為new MyPromise
,運行你會發現代碼直接報錯:
Uncaught TypeError: Cannot read properties of undefined (reading 'then')
不能從undefined
上讀取屬性then
?我不是在then
里面return
了一個new Promise
嗎?這咋回事?假設你是這樣想的,那么恭喜你,你已經成功進入了思維誤區。
我們將上面的例子代碼進行拆分:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return new Promise((resolve) => resolve(2));
});
p2.then((res) => {
console.log(res);
});
Promise
若要實現鏈式調用,那么p1.then()
一定得返回一個新的Promise
,不然下一次鏈式調用的then
從哪讀取呢?
所以這個新的Promise
是then
方法創建並提供的,而(res)=>{console.log(1);return new Promise((resolve) => resolve(2))}
這一段只是then
方法調用時的callback
,它的返回值(假設有值)將成為下次新的Promise
的value
,所以上述代碼中的return new Promise((resolve) => resolve(2))
只是在為then
創建的Promise
准備value
而已。看個例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
console.log(res);
return new Promise((resolve) => {
// 我們不改狀態
console.log("不做狀態改變的操作");
});
}).then((res) => {
console.log(res); // 這里不會輸出
});
在這個例子中,第二個then
並不會執行,這是因為p1.then()
雖然創建了一個新的Promise
,但是它依賴的value
由內部的new Promise
提供,很明顯我們並未做任何狀態改變的操作,導致第二個Promise
不會執行。
那么到這里我們能提煉出兩個非常重要的結論:
Promise
若要實現鏈式調用,then
一定得返回一個新的Promise
。- 新的
Promise
的狀態以及value
由上一個then
的callback
決定。
再次回到我們自己實現的then
方法,很明顯它並沒有創建一個新Promise
,函數沒返回值默認返回undefined
,這就解釋了為啥報這個錯了。
好了,解釋完了我們得再次改造我們的MyPromise
,為then
提供返回Promise
的操作,以及對於then
的callback
結果的處理:
const resolvePromise = (result, resolve, reject) => {
// 判斷result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
// ....
then = (fulfilledFn, rejectedFn) => {
// 我們得在每次調用then時返回一個Promise
return new MyPromise((resolve, reject) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
// 上一個then的callback的結果將作為新Promise的值
const result = callbackMap[this.status](this.value);
resolvePromise(result, resolve, reject);
});
};
}
經過這樣修改,再次運行代碼,我們發現then
鏈式調用已經成功了!!!!
我知道上面這段代碼有同學又懵了,我建議先看看上面對於then
鏈式調用我們得出的兩個結論,然后我再用兩個例子來解釋這段代碼為什么要這樣寫,別着急,我會解釋的非常清楚。
對於then
返回一個Promise
的修改這一點大家肯定沒問題,疑惑的點應該都在新增的resolvePromise
方法中。其實在前面我們解釋過了,第一個then
回調返回結果(函數沒返回默認就是undefined
),會作為下一個新Promise
的參數,而這個返回的結果它可能是一個數字,一個字符串,也可能是一個Promise
(上面的例子就是返回了一個promise
作為參數),先看一個簡單的例子:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
return 520;
}).then((res) => {
console.log(res);// 520
});
這個例子的第一個then
的callback
直接返回了一個數字,但奇怪的是下一個then
居然能拿到這個結果,這是因為上述代碼等同於:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
p1.then((res) => {
return Promise.resolve(520);
}).then((res) => {
console.log(res);// 520
});
沒錯,這也是Promise
的特性之一,如果我們的then
的回調返回的是一個非Promise
的結果,它等同於執行Promise.resolve()
,這也是為啥我們在自定義的resolvePromise
中一旦判斷result
不是Promise
就直接執行resolve
的緣故。
強化理解,來給大家看個更離譜的例子:
Promise.resolve()
.then(() => {
return new Error("error!!!");
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失敗啦");
});
猜猜這段代碼最終輸出什么?輸出成功啦
,因為它等同於:
Promise.resolve()
.then(() => {
return Promise.resolve(new Error("error!!!"));
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失敗啦");
});
對於Promise
而言,它只是一個type
類型是錯誤的value
而已,當然執行成功回調。有同學可能就要問了,那這個例子假設我就是想執行catch
咋辦?兩種寫法:
Promise.resolve()
.then(() => {
// 第一種辦法,直接reject
return Promise.reject(new Error("error!!!"));
// 第二種辦法,直接拋出錯誤
// throw new Error('error!!!')
})
.then((res) => {
console.log("成功啦");
})
.catch((err) => {
console.log("失敗啦");
});
解釋了resolvePromise
中的resolve(result)
,再來解釋下為什么result
是Promise
時執行result.then(resolv,reject)
就可以了。
我們已知回調的結果會作為下一個Promise
的參數,那假設這個參數自身就是個Promise
,對於then
返回的新Promise
而言,它就得等着作為參數的Promise
狀態改變,在上面我們已經演過參數是Promise
但不會改變狀態的例子,結果就是下一個then
不會調用。
所以對於下一個新Promise
而言,我就等着參數自己送到嘴里來,你狀態變不變,以及成功或者失敗那是你自己的事,因此我們通過result.then()
來等待這個參數Promise
的狀態變化,只要你狀態變了,比如resolve
了,那是不是得執行this.resolve
方法,從而將值賦予給this.value
,那么等到下一次執行then
時自然就能讀取對應this.value
了,是不是很巧妙?
另外,result.then(resolve, reject);
這一句代碼其實是如下代碼的簡寫,不信大家可以寫個小例子驗證下:
result.then((res)=> resolve(res), (err)=> reject(err));
算了,我猜測你們可能還是懶得寫例子驗證,運行下如下代碼就懂了,其實是一個意思:
// 定時器是支持傳遞參數的
setTimeout(console.log, 1000, '聽風是風')
// 等同於
setTimeout((param)=> console.log(param), 1000, '聽風是風')
那么上面的簡寫,其實也是這個意思,然后我們畫張圖總結下上面的結論:

恭喜你,模擬Promise
最為繞的一部分你弄清楚了,剩下的模擬都是小魚小蝦,我們繼續。
貳 ❀ 柒 增加then不能返回Promise自己的判斷
直接看個例子,這個代碼執行報錯:
const p1 = new Promise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return p2;
});
Uncaught (in promise) TypeError: Chaining cycle detected for promise #
結合上面我們自己實現then
的理解,p1.then()
返回了一個Promise p2
,結果p2
又成p2
自己需要等待的參數,說直白點就是p2
等待p2
的變化,自己等自己直接陷入死循環了。對於這個問題感興趣的同學可以看看segmentfault中對於這個問題的解答 關於promise then的問題。
我們也來模擬這個錯誤的捕獲,直接上代碼:
const resolvePromise = (p, result, resolve, reject) => {
// 判斷是不是自己,如果是調用reject
if (p === result) {
reject(new Error("Chaining cycle detected for promise #<Promise>"));
}
// 判斷result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
// ....
then = (fulfilledFn, rejectedFn) => {
// 我們得在每次調用then時返回一個Promise
const p = new MyPromise((resolve, reject) => {
const callbackMap = {
[FULFILLED]: fulfilledFn,
[REJECTED]: rejectedFn,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledFn);
this.rejectedCallback.push(rejectedFn);
},
};
// 上一個then的callback的結果將作為新Promise的值
const result = callbackMap[this.status](this.value);
// 新增了一個p,用於判斷是不是自己
resolvePromise(p, result, resolve, reject);
});
return p;
};
}
執行上面的代碼,結果又報錯....
index.html:159 Uncaught ReferenceError: Cannot access 'p2' before initialization
錯誤說我們不能在p2
初始化好之前調用它,其實看上面那個代碼本身就很奇怪,哪有在產生自己的函數的callback
中使用自己的,但這就是Promise
的特性之一,咱也沒辦法。
現在思路就是讓resolvePromise(p, result, resolve, reject)
這一句執行晚一點,起碼要晚於新Promise
的產生,咋辦?當然是用異步,比如定時器。但我們知道Promise
的then
是微任務,為了更好的模擬這個異步行為,這里借用一個API,名為queueMicrotask,想詳細了解的同學可以點擊鏈接跳轉MDN,這里我們直接上個簡單的例子:
queueMicrotask(() => {
console.log("我是異步的微任務");
});
setTimeout(() => console.log("我是異步的宏任務"));
console.log("我是同步的宏任務");

看來這個API非常符合我們的預期,因為需要考慮pending
狀態暫存函數的行為,我們還是額外封裝兩個成功與失敗的微任務,繼續改造:
then = (fulfilledFn, rejectedFn) => {
// 我們得在每次調用then時返回一個Promise
const p = new MyPromise((resolve, reject) => {
// 封裝成功的微任務
const fulfilledMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
// 獲取成功回調函數的執行結果
const result = fulfilledFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, result, resolve, reject);
});
};
// 封裝失敗的微任務
const rejectedMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
// 調用失敗回調,並且把原因返回
const result = rejectedFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, result, resolve, reject);
});
};
const callbackMap = {
[FULFILLED]: fulfilledMicrotask,
[REJECTED]: rejectedMicrotask,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledMicrotask);
this.rejectedCallback.push(rejectedMicrotask);
},
};
callbackMap[this.status]();
});
return p;
};
好了,現在執行下面這段代碼來檢驗下效果:
const p1 = new MyPromise((resolve, reject) => {
resolve(1);
});
const p2 = p1.then((res) => {
console.log(res);
return p2;
});
p2.then(
() => {},
(err) => console.log(err)
);

有同學就要說了,你這不對啊,原生Promise
是直接就報錯,你這還要p2.then()
才能感知報錯。咱前面就說了,這是在模擬仿寫Promise
,大致達到這個效果,而且這個小節的核心目的其實是為了引出then
中callback
執行為什么是異步的原因。
貳 ❀ 捌 添加new Promise以及then執行錯誤的捕獲
我們知道new Promise
或者then
回調執行報錯是,then
的錯誤回調是能成功捕獲的,我們也來模擬這個過程,這個好理解一點我們就直接上代碼:
class MyPromise {
constructor(fn) {
try {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
// ....
then = (fulfilledFn, rejectedFn) => {
// 我們得在每次調用then時返回一個Promise
const p = new MyPromise((resolve, reject) => {
// 封裝成功的微任務
const fulfilledMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
// 添加錯誤捕獲
try {
// 獲取成功回調函數的執行結果
const result = fulfilledFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
});
};
// 封裝失敗的微任務
const rejectedMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
// 添加錯誤捕獲
try {
// 調用失敗回調,並且把原因返回
const result = rejectedFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, result, resolve, reject);
} catch (e) {
reject(e);
}
});
};
// ....
});
return p;
};
}
執行如下例子,效果很理想:
const p1 = new MyPromise((resolve, reject) => {
throw new Error("new報錯啦");
});
const p2 = p1.then(
(res) => {
console.log(res);
},
(err) => {
console.log("我是錯誤回調", err);
throw new Error("then報錯啦");
}
);
p2.then(
() => {},
(err) => console.log("我是錯誤回調", err)
);

貳 ❀ 玖 實現then無callback,或者callback不是函數時的值穿透
看標題可能不明白什么意思,看個例子就懂了:
const p1 = new Promise((resolve, reject) => {
resolve("聽風");
});
const fn = () => {};
p1.then(fn()) // 函數調用,並不是一個函數
.then(1) // 數字
.then('2') // 字符串
.then() // 不傳遞
.then((res) => console.log(res)); // 聽風
說通俗一點就是,假設then
沒有回調,或者回調根本不是一個函數,那么你就當這個then
不存在,但我們的MyPromise
很明顯沒考慮無回調的情況,現在實現這一點:
then = (fulfilledFn, rejectedFn) => {
// 新增回調判斷,如果沒傳遞,那我們就定義一個單純起value接力作用的函數
fulfilledFn =
typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
rejectedFn =
typeof rejectedFn === "function"
? rejectedFn
: (value) => {
throw value;
};
// 我們得在每次調用then時返回一個Promise
const p = new MyPromise((resolve, reject) => {
// ...
});
return p;
};
上述代碼做的事情非常簡單,檢查兩個回調是不是函數,不是函數我們就幫它定義一個只做值接力的函數,你傳遞什么我們就原封不動返回什么的函數。為啥rejectedFn
要定義成(value)=>{throw value}
呢?這是因為我們希望當此函數執行時能走reject
路線,所以一定得拋錯,那為什么不寫成(value)=>{throw new Error(value)}
這樣?因為.then().then()
這種會導致new Error
執行多次,結果就不對了。我們在貳 ❀ 陸小節,提到有兩種辦法可以在報錯時讓catch
捕獲,一種是直接reject()
,另一種就是throw
一個錯誤,后面的throw
影響更小一點,所以就用這種。
經過上面的修改,此時我們再執行我們無回調的例子,此時不管是成功還是失敗,都能成功執行了。
貳 ❀ 拾 實現靜態resolve與reject
創建Promise
除了new Promise
之外,其實還能通過Promise.resolve()
靜態方法直接獲取,但目前MyPromise
只提供了實例方法,所以我們需要補全靜態方法:
class MyPromise {
// ....
// 靜態resolve
static resolve(value) {
// 加入蠶食是一個promise,原封不動的返回
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
// 靜態reject
static reject(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
reject(value);
});
}
// ....
}
邏輯也很簡單,如果參數是一個Promise
,那就原封不動返回,如果不是,我們就手動幫他創建一個Promise
即可,這個特性可以通過下面這個例子驗證:
const p1 = new Promise((resolve, reject) => {
resolve("聽風");
});
const p2 = new Promise((resolve, reject) => {
resolve("我是一個promise");
});
p1.then(
(res) => {
return Promise.resolve(p2);
},
(err) => console.log(err)
).then(
(res) => console.log(res), // 我是一個promise
(err) => console.log(err)
);
可以看到假設Promise.resolve
參數本身就是一個Promise
時,這個方法本質上就想啥也沒做一樣,但如果參數是一個數字,它會幫你包裝成一個Promise
,我們將上述代碼的new Promise
改成new MyPromise
,效果完全一致,說明模擬的很理想!!
OK,那么到這里,一個滿足基本功能的MyPromise
就實現完畢了,但事先說明,它並未符合Promise A+
規范,如果要做到一樣,我們仍需要對then
方法中做一些條件判斷,這些邏輯都是規范明確告訴你應該怎么寫,沒有什么道理可言,但鑒於這段邏輯補全對於我們理解上面的題不會有額外的幫助,因此我就不做額外的改造了,下面是一份實現到現在完整的MyPromise
代碼:
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
const resolvePromise = (p, result, resolve, reject) => {
if (p === result) {
reject(new Error("Chaining cycle detected for promise #<Promise>"));
}
// 判斷result是不是promise
if (result instanceof MyPromise) {
result.then(resolve, reject);
} else {
resolve(result);
}
};
class MyPromise {
constructor(fn) {
try {
// 這里的fn其實就是new Promise傳遞的函數
fn(this.resolve, this.reject);
} catch (e) {
this.reject(e);
}
}
// 初始化狀態以及成功,失敗的值
status = PENDING;
value = null;
// 新增記錄成功與失敗回調的參數
fulfilledCallback = [];
rejectedCallback = [];
// 靜態resolve
static resolve(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
resolve(value);
});
}
// 靜態reject
static reject(value) {
if (value instanceof MyPromise) {
return value;
}
return new MyPromise((resolve, reject) => {
reject(value);
});
}
resolve = (value) => {
// 當調用resolve時修改狀態成fulfilled,同時記錄成功的值
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED;
// 新增成功回調的調用
while (this.fulfilledCallback.length) {
this.fulfilledCallback.shift()?.(value);
}
}
};
reject = (reason) => {
// 當調用reject時修改狀態成rejected,同時記錄失敗的理由
if (this.status === PENDING) {
this.value = reason;
this.status = REJECTED;
// 新增失敗回調的調用
while (this.rejectedCallback.length) {
this.rejectedCallback.shift()?.(reason);
}
}
};
then = (fulfilledFn, rejectedFn) => {
// 新增回調判斷,如果沒傳遞,那我們就定義一個單純起value接力作用的函數
fulfilledFn =
typeof fulfilledFn === "function" ? fulfilledFn : (value) => value;
rejectedFn =
typeof rejectedFn === "function"
? rejectedFn
: (value) => {
throw value;
};
// 我們得在每次調用then時返回一個Promise
const p = new MyPromise((resolve, reject) => {
// 封裝成功的微任務
const fulfilledMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
try {
// 獲取成功回調函數的執行結果
const x = fulfilledFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
// 封裝失敗的微任務
const rejectedMicrotask = () => {
// 創建一個微任務等待 promise2 完成初始化
queueMicrotask(() => {
try {
// 調用失敗回調,並且把原因返回
const x = rejectedFn(this.value);
// 傳入 resolvePromise 集中處理
resolvePromise(p, x, resolve, reject);
} catch (error) {
reject(error);
}
});
};
const callbackMap = {
[FULFILLED]: fulfilledMicrotask,
[REJECTED]: rejectedMicrotask,
// 針對異步問題,新增pending狀態時記錄並保存回調的操作
[PENDING]: () => {
this.fulfilledCallback.push(fulfilledMicrotask);
this.rejectedCallback.push(rejectedMicrotask);
},
};
callbackMap[this.status]();
});
return p;
};
}
代碼看着有點多,但事實上順着思路寫下來,其實沒有什么很大的難點。
叄 ❀ 重回面試題
MyPromise
實現完畢,現在讓我們回頭再看看第一道題,現在再來分析為什么這么輸出,為了方便,我將題目加在下方:
const fn = (s) => (
new Promise((resolve, reject) => {
if (typeof s === 'number') {
resolve();
} else {
reject();
}
})
.then(
res => console.log('參數是一個number'),
// 注意,這里沒定義失敗回調
)
.catch(err => console.log('參數是一個字符串'))
)
fn('1');
fn(1);
叄 ❀ 壹 第一輪執行
我們先考慮同步執行,首先我們執行fn('1')
,此時執行new Promise
,因為這個過程是一個同步行為,因此它會立馬調用傳遞給Promise
的回調,然后走邏輯判斷,因為不是一個數字,導致執行了reject()
。
緊接着執行.then
,前文也說了.then
注冊微任務的行為是同步,但需要注意的是,.then
中並未提供失敗函調,因此對於Promise
底層而言,它要做的是值和狀態的穿透,這些先不管,畢竟我們還有剩余的同步任務沒走完。
於是緊接着,我們又執行了fn(1)
,同樣同步執行.then()
注冊了成功的回調,到這里,同步任務全部執行完成。
叄 ❀ 貳 第二輪執行
由於同步代碼全部跑完了,此時肯定得按照我們注入的微任務順序,依次執行微任務,由於fn('1')
這一步的.then()
沒有失敗回調,默認理解為執行了值穿透的步驟,於是返回的新Promise
的狀態依舊是rejected
且值為undefined
(因為reject
沒傳值)。
緊接着,我們執行fn(1)
的成功回調,於是先輸出了參數是一個number
,注意,這個成功回調只有一個console
,並無返回,我們默認理解為return resolve(undefined)
,因此返回了一個狀態是成功,但是值是undefined
的新Promise
。
叄 ❀ 叄 第三輪執行
兩次調用的.then
又返回了兩個新promise
,因為狀態一開始都改變了,所以還是先走rejected
的Promise
,並成功觸發.catch
,此時輸出參數是一個字符串
,而第二個Promise
是成功狀態,不能觸發.catch
,到此執行結束。
為了更好理解值穿透的解釋,我們改改代碼:
const fn = (s) => {
new Promise((resolve, reject) => {
if (typeof s === "number") {
resolve(1);
} else {
reject(2);
}
})
.then(
(res) => console.log("參數是一個number")) // 注意,這里雖然提供了函數,但是沒返回,所以理解為 return resolve(undefined)
// 注意,這里沒傳遞失敗函數,只要callback不是一個函數,默認值穿透拿上一步的promise
.then(
(succ) => console.log(succ) // 這里一定輸出undefined,畢竟上一步沒返回值,默認理解成resolve(undefined)
)
.catch((err) => {
console.log("參數是一個字符串");
console.log(err); // 這里輸出2,因為上一個then又沒失敗回調,一直穿透下來
});
};
fn("1");
fn(1);
// 參數是一個number
// undefined
// 參數是一個字符串
// 2
而假設我們有為then
提供失敗回調,那么此時返回的順序就符合一開始我們對於Promise
還不太了解時能夠理解的預期:
const fn = (s) => {
new Promise((resolve, reject) => {
if (typeof s === "number") {
resolve();
} else {
reject();
}
})
.then(
(res) => console.log("參數是一個number"),
(err) => console.log("參數是一個字符串11")
)
.catch((err) => {
console.log("參數是一個字符串");
// 看看上一個then傳遞的value是啥
console.log(err);
});
};
fn("1");
fn(1);

因為有提供失敗回調,這就導致.catch
不會執行了。那么到這里,第一道面試題算是非常透徹的解釋完了,也多虧手寫Promise
加深了對於底層原理的理解。
我們接着聊第二道題,為了方便理解,我們將這道題的Promise
全部改成MyPromise
,再看看輸出如何:
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
使用我們實現的MyPromise
,結果發現4
跑到了2
后面,我們可以先站在自己實現的邏輯上解釋這個現象。
我們已知.then()
會返回一個Promise
,且這個Promise
啥時候執行以及參數都是由.then()
接收的回調函數的返回結果來決定的。而在題目中MyPromise.resolve(4)
這一句,其實本質上就等同於如下代碼(參照靜態resolve
實現):
MyPromise.resolve()
.then(() => {
console.log(0);
return new MyPromise(resolve=>resolve(4));
})
.then((res) => {
console.log(res);
});
而在then
調用中最后都需要走resolvePromise
,此方法會判斷參數是否是一個Promise
,如果是就需要執行result.then()
。
不知道你腦袋里是否已經有了一種感覺,相比.then(()=>console.log(2))
,前者比后者多執行了一次.then
,也就是說多創建了一次微任務,這就導致4
一定晚於2
輸出。
但是題目2的輸出,4
其實是在3
之后,會不會有一種可能,官方Promise
中return Promise.resolve(4)
這種行為在底層其實創建了兩次微任務,導致4延遲了2次后才輸出呢?
在查證了V8中關於Promise
的源碼,直接說結論,確實是創建了兩次微任務,因為涉及到篇幅問題,若對這兩個微任務有興趣,可直接閱讀知乎問題 promise.then 中 return Promise.resolve 后,發生了什么?,有優秀答主詳細分析了源碼中兩次微任務產生的地方,只是站在我的角度,我個人覺得了解到這個結論就好,再繼續深入分析收益不成正比,所以在這我偷個懶。
肆 ❀ 總
那么到這里,一篇長達八千多字的文章也記錄完成了,本着了解兩道面試題的態度,我們嘗試手寫了一個自己的Promise
,在實現過程中,就我自己而言確實又了解了不少之前從未聽過的特性,比如Promise
不能返回自己,比如.then
返回的Promise
的執行其實依賴了.then
回調函數的結果等等。另外,我會在參考中附帶一篇我覺得很不錯的Promise
面試題集合,大家也可以在看完這篇文章后嘗試做做這里面的執行題,加深對於Promise
的理解。
另外,實際面試中基本沒有真讓你手寫Promise A+
的題,畢竟規范那么多,手寫下來難度過大,但實際面試會有讓你手寫Promise.all
或者Promise.race
類似的手寫題,后續我也會把這些手寫問題給補全,那么到這里本文結束。
推薦閱讀
超耐心地毯式分析,來試試這道看似簡單但暗藏玄機的Promise順序執行題
一個思路搞定三道Promise並發編程題,手摸手教你實現一個Promise限制器
強化Promise理解,從零手寫屬於自己的Promise.all與Promise.race
伍 ❀ 參考
從一道讓我失眠的 Promise 面試題開始,深入分析 Promise 實現細節
【V8源碼補充篇】從一道讓我失眠的 Promise 面試題開始,深入分析 Promise 實現細節
promise.then 中 return Promise.resolve 后,發生了什么?
[要就來45道Promise面試題一次爽到底](