更新說明
- 更新時間:2019/1/23
我把then方法的執行做成同步的了,是不符合規範的。
[《Promises/A+規範》][6]中,【Then 方法】小節【呼叫時機】部分寫道:“onFulfilled 和 onRejected 只有在執行環境堆疊僅包含平臺程式碼時才可被呼叫”,這裡特別要看一下注釋。
因此我要把onFulfilled
和 onRejected
的程式碼放在“ then
方法被呼叫的那一輪事件迴圈之後的新執行棧中執行”,通過setTimeout
方法將任務放到本輪任務佇列的末尾。程式碼已新增到最後一部分-第九步。
關於任務佇列的執行機制,感興趣可看一下阮一峰老師的《JavaScript 執行機制詳解:再談Event Loop》
實現功能:
- 已實現
Promise
基本功能,與原生一樣,非同步、同步操作均ok,具體包括:MyPromise.prototype.then()
MyPromise.prototype.catch()
與原生Promise
略有出入MyPromise.prototype.finally()
MyPromise.all()
MyPromise.race()
MyPromise.resolve()
MyPromise.reject()
rejected
狀態的冒泡處理也已解決,當前Promise的reject如果沒有捕獲,會一直冒泡到最後,直到catchMyPromise
狀態一旦改變,將不能再改變它的狀態
不足之處:
- 程式碼的錯誤被catch捕獲時,提示的資訊(捕獲的錯誤物件)比原生Promise要多
- 程式碼是es6寫的,會考慮再用es5寫,以便於應用到es5專案中;es5寫的話,不用箭頭函式,要考慮this的問題
測試: index.html
- 這個頁面中包含了27個測試例子,分別測試了各項功能、各個方法,還有一些特殊情況測試;或許還有有遺漏的,感興趣自己可以玩一下;
- 視覺化的操作,方便測試,每次執行一個例子,開啟除錯臺即可看到結果;建議同時開啟
index.js
邊看程式碼邊玩; - 同一套程式碼,上面的
MyPromise
的執行結果,下面是原生Promise
執行的結果;
收穫
- 這個過程很開心,能夠自己挑戰原生的東西,這是我第一回;
- 花了好多天時間去折騰
Promise
先是弄懂他,再去思考他,最後一步步把功能實現出來,懟他的理解不斷加深,越來越透徹; - 當寫到一個新功能時,發現在這個新功能裡,上一個功能有遺漏,這時候需要了解他們倆之間的關係,還要重新理解上一個功能;在這種重複當中,無疑又加深了一層理解;
then/catch
方法是最難的,要不停地修修補補;- 最後所有功能都實現了,才想起來一個關鍵點“Promise狀態一旦確定,不能再改變”,又新增了一些邏輯才得以解決。因此,這個過程,難以做到滴水不漏,或許現在的程式碼裡還有些隱藏問題沒被發現。
reject
狀態的冒泡是個難題,但在下面的程式碼中我沒有專門提及,我也沒有辦法具體說清楚他,我是在整個過程中不停地調才最終調出來正確的冒泡結果。
程式碼
下面貼程式碼,包括整個思考過程,會有點長
為了說明書寫的邏輯,我使用以下幾個註釋標識,整坨變動的程式碼只標識這一坨的開頭處。
//++
——新增的程式碼
//-+
——修改的程式碼
第一步,定義MyPromise類
名字隨便取,我的叫MyPromise,沒有取代原生的Promise。
- 建構函式傳入回撥函式
callback
。當新建MyPromise
物件時,我們需要執行此回撥,並且callback
自身也有兩個引數,分別是resolve
和reject
,他們也是回撥函式的形式; - 定義了幾個變數儲存當前的一些結果與狀態、事件佇列,見註釋;
- 執行函式
callback
時,如果是resolve
狀態,將結果儲存在this.__succ_res
中,狀態標記為成功;如果是reject
狀態,操作類似; - 同時定義了最常用的
then
方法,是一個原型方法; - 執行
then
方法時,判斷物件的狀態是成功還是失敗,分別執行對應的回撥,把結果傳入回撥處理; - 這裡接收
...arg
和傳入引數...this.__succ_res
都使用了擴充套件運算子,為了應對多個引數的情況,原封不動地傳給then
方法回撥。
callback
回撥這裡使用箭頭函式,this
的指向就是本當前MyPromise
物件,所以無需處理this
問題。
class MyPromise {
constructor(callback) {
this.__succ_res = null; //儲存成功的返回結果
this.__err_res = null; //儲存失敗的返回結果
this.status = 'pending'; //標記處理的狀態
//箭頭函式繫結了this,如果使用es5寫法,需要定義一個替代的this
callback((...arg) => {
this.__succ_res = arg;
this.status = 'success';
}, (...arg) => {
this.__err_res = arg;
this.status = 'error';
});
}
then(onFulfilled, onRejected) {
if (this.status === 'success') {
onFulfilled(...this.__succ_res);
} else if (this.status === 'error') {
onRejected(...this.__err_res);
};
}
};
複製程式碼
到這裡,MyPromise
可以簡單實現一些同步程式碼,比如:
new MyPromise((resolve, reject) => {
resolve(1);
}).then(res => {
console.log(res);
});
//結果 1
複製程式碼
第二步,加入非同步處理
執行非同步程式碼時,then
方法會先於非同步結果執行,上面的處理還無法獲取到結果。
- 首先,既然是非同步,
then
方法在pending
狀態時就執行了,所以新增一個else
; - 執行
else
時,我們還沒有結果,只能把需要執行的回撥,放到一個佇列裡,等需要時執行它,所以定義了一個新變數this.__queue
儲存事件佇列; - 當非同步程式碼執行完畢,這時候把
this.__queue
佇列裡的回撥統統執行一遍,如果是resolve
狀態,則執行對應的resolve
程式碼。
class MyPromise {
constructor(fn) {
this.__succ_res = null; //儲存成功的返回結果
this.__err_res = null; //儲存失敗的返回結果
this.status = 'pending'; //標記處理的狀態
this.__queue = []; //事件佇列 //++
//箭頭函式繫結了this,如果使用es5寫法,需要定義一個替代的this
fn((...arg) => {
this.__succ_res = arg;
this.status = 'success';
this.__queue.forEach(json => { //++
json.resolve(...arg);
});
}, (...arg) => {
this.__err_res = arg;
this.status = 'error';
this.__queue.forEach(json => { //++
json.reject(...arg);
});
});
}
then(onFulfilled, onRejected) {
if (this.status === 'success') {
onFulfilled(...this.__succ_res);
} else if (this.status === 'error') {
onRejected(...this.__err_res);
} else { //++
this.__queue.push({resolve: onFulfilled, reject: onRejected});
};
}
};
複製程式碼
到這一步,MyPromise
已經可以實現一些簡單的非同步程式碼了。測試用例 index.html
中,這兩個例子已經可以實現了。
1 非同步測試--resolve
2 非同步測試--reject
第三步,加入鏈式呼叫
實際上,原生的 Promise
物件的then方法,返回的也是一個 Promise
物件,一個新的 Promise
物件,這樣才可以支援鏈式呼叫,一直then
下去。。。
而且,then
方法可以接收到上一個then
方法處理return的結果。根據Promise
的特性分析,這個返回結果有3種可能:
MyPromise
物件;- 具有
then
方法的物件; - 其他值。 根據這三種情況分別處理。
- 第一個處理的是,
then
方法返回一個MyPromise
物件,它的回撥函式接收resFn
和rejFn
兩個回撥函式; - 把成功狀態的處理程式碼封裝為
handle
函式,接受成功的結果作為引數; handle
函式中,根據onFulfilled
返回值的不同,做不同的處理:- 首先,先獲取
onFulfilled
的返回值(如果有),儲存為returnVal
; - 然後,判斷
returnVal
是否有then方法,即包括上面討論的1、2中情況(它是MyPromise
物件,或者具有then
方法的其他物件),對我們來說都是一樣的; - 之後,如果有
then
方法,馬上呼叫其then
方法,分別把成功、失敗的結果丟給新MyPromise
物件的回撥函式;沒有則結果傳給resFn
回撥函式。
- 首先,先獲取
class MyPromise {
constructor(fn) {
this.__succ_res = null; //儲存成功的返回結果
this.__err_res = null; //儲存失敗的返回結果
this.status = 'pending'; //標記處理的狀態
this.__queue = []; //事件佇列
//箭頭函式繫結了this,如果使用es5寫法,需要定義一個替代的this
fn((...arg) => {
this.__succ_res = arg;
this.status = 'success';
this.__queue.forEach(json => {
json.resolve(...arg);
});
}, (...arg) => {
this.__err_res = arg;
this.status = 'error';
this.__queue.forEach(json => {
json.reject(...arg);
});
});
}
then(onFulfilled, onRejected) {
return new MyPromise((resFn, rejFn) => { //++
if (this.status === 'success') {
handle(...this.__succ_res); //-+
} else if (this.status === 'error') {
onRejected(...this.__err_res);
} else {
this.__queue.push({resolve: handle, reject: onRejected}); //-+
};
function handle(value) { //++
//then方法的onFulfilled有return時,使用return的值,沒有則使用儲存的值
let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
//如果onFulfilled返回的是新MyPromise物件或具有then方法物件,則呼叫它的then方法
if (returnVal && returnVal['then'] instanceof Function) {
returnVal.then(res => {
resFn(res);
}, err => {
rejFn(err);
});
} else {//其他值
resFn(returnVal);
};
};
})
}
};
複製程式碼
到這裡,MyPromise
物件已經支援鏈式呼叫了,測試例子: 4 鏈式呼叫--resolve
。但是,很明顯,我們還沒完成reject
狀態的鏈式呼叫。
處理的思路是類似的,在定義的errBack
函式中,檢查onRejected
返回的結果是否含then
方法,分開處理。值得一提的是,如果返回的是普通值,應該呼叫的是resFn
,而不是rejFn
,因為這個返回值屬於新MyPromise
物件,它的狀態不因當前MyPromise
物件的狀態而確定。即是,返回了普通值,未表明reject
狀態,我們預設為resolve
狀態。
程式碼過長,只展示改動部分。
then(onFulfilled, onRejected) {
return new MyPromise((resFn, rejFn) => {
if (this.status === 'success') {
handle(...this.__succ_res);
} else if (this.status === 'error') {
errBack(...this.__err_res); //-+
} else {
this.__queue.push({resolve: handle, reject: errBack}); //-+
};
function handle(value) {
//then方法的onFulfilled有return時,使用return的值,沒有則使用儲存的值
let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
//如果onFulfilled返回的是新MyPromise物件或具有then方法物件,則呼叫它的then方法
if (returnVal && returnVal['then'] instanceof Function) {
returnVal.then(res => {
resFn(res);
}, err => {
rejFn(err);
});
} else {//其他值
resFn(returnVal);
};
};
function errBack(reason) { //++
if (onRejected instanceof Function) {
//如果有onRejected回撥,執行一遍
let returnVal = onRejected(reason);
//執行onRejected回撥有返回,判斷是否thenable物件
if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
returnVal.then(res => {
resFn(res);
}, err => {
rejFn(err);
});
} else {
//無返回或者不是thenable的,直接丟給新物件resFn回撥
resFn(returnVal); //resFn,而不是rejFn
};
} else {//傳給下一個reject回撥
rejFn(reason);
};
};
})
}
複製程式碼
現在,MyPromise
物件已經很好地支援鏈式呼叫了,測試例子:
4 鏈式呼叫--resolve
5 鏈式呼叫--reject
28 then回撥返回Promise物件(reject)
29 then方法reject回撥返回Promise物件
第四步,MyPromise.resolve()和MyPromise.reject()方法實現
因為其它方法對MyPromise.resolve()
方法有依賴,所以先實現這個方法。
先要完全弄懂MyPromise.resolve()
方法的特性,研究了阮一峰老師的ECMAScript 6 入門對於MyPromise.resolve()
方法的描述部分,得知,這個方法功能很簡單,就是把引數轉換成一個MyPromise
物件,關鍵點在於引數的形式,分別有:
- 引數是一個
MyPromise
例項; - 引數是一個
thenable
物件; - 引數不是具有
then
方法的物件,或根本就不是物件; - 不帶有任何引數。
處理的思路是:
- 首先考慮極端情況,引數是undefined或者null的情況,直接處理原值傳遞;
- 其次,引數是
MyPromise
例項時,無需處理; - 然後,引數是其它
thenable
物件的話,呼叫其then
方法,把相應的值傳遞給新MyPromise
物件的回撥; - 最後,就是普通值的處理。
MyPromise.reject()
方法相對簡單很多。與MyPromise.resolve()
方法不同,MyPromise.reject()
方法的引數,會原封不動地作為reject
的理由,變成後續方法的引數。
MyPromise.resolve = (arg) => {
if (typeof arg === 'undefined' || arg == null) {//無引數/null
return new MyPromise((resolve) => {
resolve(arg);
});
} else if (arg instanceof MyPromise) {
return arg;
} else if (arg['then'] instanceof Function) {
return new MyPromise((resolve, reject) => {
arg.then((res) => {
resolve(res);
}, err => {
reject(err);
});
});
} else {
return new MyPromise(resolve => {
resolve(arg);
});
}
};
MyPromise.reject = (arg) => {
return new MyPromise((resolve, reject) => {
reject(arg);
});
};
複製程式碼
測試用例有8個:18-25
,感興趣可以玩一下。
第五步,MyPromise.all()和MyPromise.race()方法實現
MyPromise.all()
方法接收一堆MyPromise
物件,當他們都成功時,才執行回撥。依賴MyPromise.resolve()
方法把不是MyPromise
的引數轉為MyPromise
物件。
每個物件執行then
方法,把結果存到一個陣列中,當他們都執行完畢後,即i === arr.length
,才呼叫resolve()
回撥,把結果傳進去。
MyPromise.race()
方法也類似,區別在於,這裡做的是一個done
標識,如果其中之一改變了狀態,不再接受其他改變。
MyPromise.all = (arr) => {
if (!Array.isArray(arr)) {
throw new TypeError('引數應該是一個陣列!');
};
return new MyPromise(function(resolve, reject) {
let i = 0, result = [];
next();
function next() {
//如果不是MyPromise物件,需要轉換
MyPromise.resolve(arr[i]).then(res => {
result.push(res);
i++;
if (i === arr.length) {
resolve(result);
} else {
next();
};
}, reject);
};
})
};
MyPromise.race = arr => {
if (!Array.isArray(arr)) {
throw new TypeError('引數應該是一個陣列!');
};
return new MyPromise((resolve, reject) => {
let done = false;
arr.forEach(item => {
//如果不是MyPromise物件,需要轉換
MyPromise.resolve(item).then(res => {
if (!done) {
resolve(res);
done = true;
};
}, err => {
if (!done) {
reject(err);
done = true;
};
});
})
})
}
複製程式碼
測試用例:
6 all方法
26 race方法測試
第六步,Promise.prototype.catch()和Promise.prototype.finally()方法實現
他們倆本質上是then
方法的一種延伸,特殊情況的處理。
catch程式碼中註釋部分是我原來的解決思路:執行catch時,如果已經是錯誤狀態,則直接執行回撥;如果是其它狀態,則把回撥函式推入事件佇列,待最後接收到前面reject狀態時執行;因為catch直接收reject狀態,所以佇列中resolve是個空函式,防止報錯。
後來看了參考文章3才瞭解到還有更好的寫法,因此替換了。
class MyPromise {
constructor(fn) {
//...略
}
then(onFulfilled, onRejected) {
//...略
}
catch(errHandler) {
// if (this.status === 'error') {
// errHandler(...this.__err_res);
// } else {
// this.__queue.push({resolve: () => {}, reject: errHandler});
// //處理最後一個Promise的時候,佇列resolve推入一個空函式,不造成影響,不會報錯----如果沒有,則會報錯
// };
return this.then(undefined, errHandler);
}
finally(finalHandler) {
return this.then(finalHandler, finalHandler);
}
};
複製程式碼
測試用例:
7 catch測試
16 finally測試——非同步程式碼錯誤
17 finally測試——同步程式碼錯誤
第七步,程式碼錯誤的捕獲
目前而言,我們的catch
還不具備捕獲程式碼報錯的能力。思考,錯誤的程式碼來自於哪裡?肯定是使用者的程式碼,2個來源分別有:
MyPromise
物件建構函式回撥then
方法的2個回撥 捕獲程式碼執行錯誤的方法是原生的try...catch...
,所以我用它來包裹這些回撥執行,捕獲到的錯誤進行相應處理。
為確保程式碼清晰,提取了
resolver
、rejecter
兩個函式,因為是es5寫法,需要手動處理this
指向問題
class MyPromise {
constructor(fn) {
this.__succ_res = null; //儲存成功的返回結果
this.__err_res = null; //儲存失敗的返回結果
this.status = 'pending'; //標記處理的狀態
this.__queue = []; //事件佇列
//定義function需要手動處理this指向問題
let _this = this; //++
function resolver(...arg) { //++
_this.__succ_res = arg;
_this.status = 'success';
_this.__queue.forEach(json => {
json.resolve(...arg);
});
};
function rejecter(...arg) { //++
_this.__err_res = arg;
_this.status = 'error';
_this.__queue.forEach(json => {
json.reject(...arg);
});
};
try { //++
fn(resolver, rejecter); //-+
} catch(err) { //++
this.__err_res = [err];
this.status = 'error';
this.__queue.forEach(json => {
json.reject(...err);
});
};
}
then(onFulfilled, onRejected) {
//箭頭函式繫結了this,如果使用es5寫法,需要定義一個替代的this
return new MyPromise((resFn, rejFn) => {
function handle(value) {
//then方法的onFulfilled有return時,使用return的值,沒有則使用回撥函式resolve的值
let returnVal = value; //-+
if (onFulfilled instanceof Function) { //-+
try { //++
returnVal = onFulfilled(value);
} catch(err) { //++
//程式碼錯誤處理
rejFn(err);
return;
}
};
if (returnVal && returnVal['then'] instanceof Function) {
//如果onFulfilled返回的是新Promise物件,則呼叫它的then方法
returnVal.then(res => {
resFn(res);
}, err => {
rejFn(err);
});
} else {
resFn(returnVal);
};
};
function errBack(reason) {
//如果有onRejected回撥,執行一遍
if (onRejected instanceof Function) {
try { //++
let returnVal = onRejected(reason);
//執行onRejected回撥有返回,判斷是否thenable物件
if (typeof returnVal !== 'undefined' && returnVal['then'] instanceof Function) {
returnVal.then(res => {
resFn(res);
}, err => {
rejFn(err);
});
} else {
//不是thenable的,直接丟給新物件resFn回撥
resFn(returnVal);
};
} catch(err) { //++
//程式碼錯誤處理
rejFn(err);
return;
}
} else {//傳給下一個reject回撥
rejFn(reason);
};
};
if (this.status === 'success') {
handle(...this.__succ_res);
} else if (this.status === 'error') {
errBack(...this.__err_res);
} else {
this.__queue.push({resolve: handle, reject: errBack});
};
})
}
};
複製程式碼
測試用例:
11 catch測試——程式碼錯誤捕獲
12 catch測試——程式碼錯誤捕獲(非同步)
13 catch測試——then回撥程式碼錯誤捕獲
14 catch測試——程式碼錯誤catch捕獲
其中第12個非同步程式碼錯誤測試,結果顯示是直接報錯,沒有捕獲錯誤,原生的Promise
也是這樣的,我有點不能理解為啥不捕獲處理它。
第八步,處理MyPromise狀態確定不允許再次改變
這是Promise
的一個關鍵特性,處理起來不難,在執行回撥時加入狀態判斷,如果已經是成功或者失敗狀態,則不執行回撥程式碼。
class MyPromise {
constructor(fn) {
this.__succ_res = null; //儲存成功的返回結果
this.__err_res = null; //儲存失敗的返回結果
this.status = 'pending'; //標記處理的狀態
this.__queue = []; //事件佇列
//箭頭函式繫結了this,如果使用es5寫法,需要定義一個替代的this
let _this = this;
function resolver(...arg) {
if (_this.status === 'pending') { //++
//如果狀態已經改變,不再執行本程式碼
_this.__succ_res = arg;
_this.status = 'success';
_this.__queue.forEach(json => {
json.resolve(...arg);
});
};
};
function rejecter(...arg) {
if (_this.status === 'pending') { //++
//如果狀態已經改變,不再執行本程式碼
_this.__err_res = arg;
_this.status = 'error';
_this.__queue.forEach(json => {
json.reject(...arg);
});
};
};
try {
fn(resolver, rejecter);
} catch(err) {
this.__err_res = [err];
this.status = 'error';
this.__queue.forEach(json => {
json.reject(...err);
});
};
}
//...略
};
複製程式碼
測試用例:
27 Promise狀態多次改變
第九步,onFulfilled 和 onRejected 方法非同步執行
到這裡為止,如果執行下面一段程式碼,
function test30() {
function fn30(resolve, reject) {
console.log('running fn30');
resolve('resolve @fn30')
};
console.log('start');
let p = new MyPromise(fn30);
p.then(res => {
console.log(res);
}).catch(err => {
console.log('err=', err);
});
console.log('end');
};
複製程式碼
輸出結果是:
//MyPromise結果
// start
// running fn30
// resolve @fn30
// end
//原生Promise結果:
// start
// running fn30
// end
// resolve @fn30
複製程式碼
兩個結果不一樣,因為onFulfilled 和 onRejected 方法不是非同步執行的,需要做以下處理,將它們的程式碼放到本輪任務佇列的末尾執行。
function MyPromise(callback) {
//略……
var _this = this;
function resolver(res) {
setTimeout(() => { //++ 利用setTimeout調整任務執行佇列
if (_this.status === PENDING) {
_this.status = FULFILLED;
_this.__succ__res = res;
_this.__queue.forEach(item => {
item.resolve(res);
});
};
}, 0);
};
function rejecter(rej) {
setTimeout(() => { //++
if (_this.status === PENDING) {
_this.status = REJECTED;
_this.__err__res = rej;
_this.__queue.forEach(item => {
item.reject(rej);
});
};
}, 0);
};
//略……
};
複製程式碼
測試用例:
30 then方法的非同步執行
以上,是我所有的程式碼書寫思路、過程。完整程式碼與測試程式碼到github下載