解析 Promise 原理,實現一個Promise
概述
這篇文章旨在解析 Promise的非同步實現原理,並且以 ES6中的 Promise 為藍本實現一個簡單的 Promise。
通過自己動手實現一個 Promise 物件,可以熟悉很多可能不知道的 Promise 細節,同時也能對非同步的理解更提升一步。
本文假設讀者對 Promise 規範有一定理解,並且熟悉 ES6 中的 Promise 基本操作。
Promise 核心
Promise 概括來說是對非同步的執行結果的描述物件。(這句話的理解很重要)
Promise 規範中規定了,promise 的狀態只有3種:
- pending
- fulfilled
- rejected
顧名思義,對上面3個狀態的解釋就不再贅述,Promise 的狀態一旦改變則不會再改變
- Promise 規範中還規定了 Promise 中必須有 then 方法,這個方法也是實現非同步的鏈式操作的基本。
ES6 Promise細節
- Promise 構造器中必須傳入函式,否則會丟擲錯誤。(沒有執行器還怎麼做非同步操作。。。)
- Promise.prototype上的 catch(onrejected) 方法是 then(null,onrejected) 的別名,並且會處理鏈之前的任何的reject。
- Promise.prototype 上的 then和 catch 方法總會返回一個全新的 Promise 物件。
- 如果傳入構造器的函式中丟擲了錯誤,該 promise 物件的[[PromiseStatus]]會賦值為 rejected,並且[[PromiseValue]]賦值為 Error 物件。
- then 中的回撥如果丟擲錯誤,返回的 promise 物件的[[PromiseStatus]]會賦值為 rejected,並且[[PromiseValue]]賦值為 Error 物件。
- then 中的回撥返回值會影響 then 返回的 promise 物件。(下文會具體分析)
動手實現
做了上面的鋪墊,實現一個 Promise 的思路就清晰很多了,本文使用 ES6 來進行實現,暫且把這個類取名為 GPromise吧(不覆蓋原生的,便於和原生進行對比測試)。下文中 GPromise 代指將要實現的類,Promise 代指 ES6中的 Promise 類。
內部屬性
在瀏覽器中打印出一個 Promise 例項會發現其中會包括兩用”[[ ]]”包裹起來的屬性,這是系統內部屬性,只有JS 引擎能夠訪問。
[[PromiseStatus]]
[[PromiseValue]]
以上兩個屬性分別是 Promise 物件的狀態和最終值。
我們自己不能實現內部屬性,JS中私有屬性特性(#修飾符現在還是提案)暫時也沒有支援,所以暫且用”_”字首規定私有屬性,這樣就模擬了Promise 中的兩個內部屬性。
class GPromise {
constructor(executor) {
this._promiseStatus = GPromise.PENDING;
this._promiseValue;
this.execute(executor);
}
execute(executor){
//...
}
then(onfulfilled, onrejected){
//...
}
}
GPromise.PENDING = 'pedding';
GPromise.FULFILLED = 'resolved';
GPromise.REJECTED = 'rejected';
執行器
- 傳入構造器的executor為函式,並且在構造時就會執行。
- 我們給 executor 中傳入 resolve 和 reject 引數,這兩個引數都是函式,用於改變改變 _promiseStatus和 _promiseValue 的值。
- 並且內部做了捕獲異常的操作,一旦傳入的executor 函式執行丟擲錯誤,GPromise 例項會變成 rejected狀態,即 _promiseStatus賦值為’rejected’,並且 _promiseValue賦值為Error物件。
execute(executor) {
if (typeof executor != 'function') {
throw new Error(` GPromise resolver ${executor} is not a function`);
}
//捕獲錯誤
try {
executor(data => {
this.promiseStatus = GPromise.FULFILLED;
this.promiseValue = data;
}, data => {
this.promiseStatus = GPromise.REJECTED;
this.promiseValue = data;
});
} catch (e) {
this.promiseStatus = GPromise.REJECTED;
this.promiseValue = e;
}
}
注:Promise 物件在executor 發生錯誤或者reject 時,如果沒有then
或者 catch 來處理,會把錯誤丟擲到外部,也就是會報錯。GPromise 實現的是沒有向外部丟擲錯誤,只能由then方法處理。
then方法
非同步實現
then 方法內部邏輯稍微複雜點,並且有一點一定一定一定要注意到: then 方法中的回撥是非同步執行的,思考下下段程式碼:
console.log(1);
new Promise((resolve,reject)=>{
console.log(2);
resolve();
})
.then(()=>console.log(3));
console.log(4);
執行結果是什麼呢?答案其實是:1 2 4 3。傳入Promise 中的執行函式是立即執行完的啊,為什麼不是立即執行 then 中的回撥呢?因為then 中的回撥是非同步執行,表示該回調是插入事件佇列末尾,在當前的同步任務結束之後,下次事件迴圈開始時執行佇列中的任務。
then 方法中的難點就是處理非同步,其中一個方案是通過 setInterval來監聽GPromise 物件的狀態改變,一旦改變則執行相應then 中相應的回撥函式(onfulfilled和onrejected),這樣回撥函式就能夠插入事件佇列末尾,非同步執行,實驗證明可行,這種方案是最直觀也最容易理解的。
then 返回值
then 方法的返回值是一個新的 GPromise 物件,並且這個物件的狀態和 then 中的回撥返回值相關,回撥指代傳入的 onfulfilled 和 rejected。
1. 如果 then 中的回撥丟擲了錯誤,返回的 GPromise 的 _promiseStatus 賦值為’rejected’, _promiseValue賦值為丟擲的錯誤物件。
2. 如果回撥返回了一個非 GPromise 物件, then返回的 GPromise 的 _promiseStatus 賦值為’resolved’, _promiseValue賦值為回撥的返回值。
3. 如果回撥返回了一個 GPromise 物件,then返回的GPromise物件 的_promiseStatus和 _promiseValue 和其保持同步。也就是 then 返回的GPromise記錄了回撥返回的狀態和值,不是直接返回回撥的返回值。
程式碼
then 方法中的重點邏輯如上,其他參見程式碼即可:
then(onfulfilled, onrejected) {
let _ref = null,
timer = null,
result = new GPromise(() => {});
//因為 promise 的 executor 是非同步操作,需要監聽 promise 物件狀態變化,並且不能阻塞執行緒
timer = setInterval(() => {
if ((typeof onfulfilled == 'function' && this._promiseStatus == GPromise.FULFILLED) ||
(typeof onrejected == 'function' && this._promiseStatus == GPromise.REJECTED)) {
//狀態發生變化,取消監聽
clearInterval(timer);
//捕獲傳入 then 中的回撥的錯誤,交給 then 返回的 promise 處理
try {
if (this._promiseStatus == GPromise.FULFILLED) {
_ref = onfulfilled(this._promiseValue);
} else {
_ref = onrejected(this._promiseValue);
}
//根據回撥的返回值來決定 then 返回的 GPromise 例項的狀態
if (_ref instanceof GPromise) {
//如果回撥函式中返回的是 GPromise 例項,那麼需要監聽其狀態變化,返回新例項的狀態是根據其變化相應的
timer = setInterval(()=>{
if (_ref._promiseStatus == GPromise.FULFILLED ||
_ref._promiseStatus == GPromise.REJECTED) {
clearInterval(timer);
result._promiseValue = _ref._promiseValue;
result._promiseStatus = _ref._promiseStatus;
}
},0);
} else {
//如果返回的是非 GPromise 例項
result._promiseValue = _ref;
result._promiseStatus = GPromise.FULFILLED;
}
} catch (e) {
//回撥中丟擲錯誤的情況
result._promiseStatus = GPromise.REJECTED;
result._promiseValue = e;
}
}
}, 0);
//promise 之所以能夠鏈式操作,因為返回了GPromise物件
return result;
}
測試用例
是騾子是馬,拉出來溜溜。。
測試環境是macOS Sierra 10.12.6,Chrome 60.0.3112.113。
經過以下測試, 證明了GPromise 的基本的非同步流程管理和原生 Promise 沒有差別。以下測試用例參考了 MDN 中的Promise
API 中的 Advanced Example。
var promiseCount = 0;
function test(isPromise) {
let thisPromiseCount = ++promiseCount,
executor = (resolve, reject) => {
console.log(thisPromiseCount + ') Promise started (Async code started)');
window.setTimeout(
function () {
resolve(thisPromiseCount);
}, Math.random() * 2000 + 1000);
};
console.log(thisPromiseCount + ') Started (Sync code started)');
let p1 = isPromise ? new Promise(executor) : new GPromise(executor);
p1.then(
function (val) {
console.log(val + ') Promise fulfilled (Async code terminated)');
},
function (reason) {
console.log('Handle rejected promise (' + reason + ') here.');
});
console.log(thisPromiseCount + ') Promise made (Sync code terminated)');
}
test();
test(true);
test();
那麼再來測試下鏈式操作(沒有鏈式操作的 Promise 我要你有何用?),測試結果和 Promise 表現一致。
function async1() {
return new GPromise(
(resolve, reject) => {
console.log('async1 start');
setTimeout(() => {
resolve('async1 finished')
}, 1000);
}
);
}
function async2() {
return new GPromise(
(resolve, reject) => {
console.log('async2 start');
setTimeout(() => {
resolve('async2 finished')
}, 1000);
}
);
}
function async3() {
return new GPromise(
(resolve, reject) => {
console.log('async3 start');
setTimeout(() => {
resolve('async3 finished');
}, 1000);
}
);
}
async1()
.then(
data => {
console.log(data);
return async2();
})
.then(
data => {
console.log(data);
return async3();
}
)
.then(
data => {
console.log(data);
}
);
總結
到此為止,一個高仿的 Promise 已經實現完成了,它很簡單,因為只有一個 then 方法,非同步的狀態管理由內部完成。
這裡並沒有實現 catch方法,因為上文也提到了,catch方法就相當於 then(null,onrejected) 。而且 Promise 類上的 race,all,resolve,reject也沒有實現,本文旨在理清 Promise 核心原理,篇幅受限(其實就是我懶),其他輔助類的方法等之後有時間再實現。
本文提供的只是一個思路,希望能幫助到你,歡迎大家批評指教。