我把then方法的執行作成同步的了,是不符合規範的。javascript
[《Promises/A+規範》][6]中,【Then 方法】小節【調用時機】部分寫道:「onFulfilled 和 onRejected 只有在執行環境堆棧僅包含平臺代碼時纔可被調用」,這裏特別要看一下注釋。html
所以我要把onFulfilled
和 onRejected
的代碼放在「 then
方法被調用的那一輪事件循環以後的新執行棧中執行」,經過setTimeout
方法將任務放到本輪任務隊列的末尾。代碼已添加到最後一部分-第九步。java
關於任務隊列的運行機制,感興趣可看一下阮一峯老師的《JavaScript 運行機制詳解:再談Event Loop》git
Promise
基本功能,與原生同樣,異步、同步操做均ok,具體包括:
MyPromise.prototype.then()
MyPromise.prototype.catch()
與原生 Promise
略有出入MyPromise.prototype.finally()
MyPromise.all()
MyPromise.race()
MyPromise.resolve()
MyPromise.reject()
rejected
狀態的冒泡處理也已解決,當前Promise的reject若是沒有捕獲,會一直冒泡到最後,直到catchMyPromise
狀態一旦改變,將不能再改變它的狀態index.html
index.js
邊看代碼邊玩;MyPromise
的運行結果,下面是原生 Promise
運行的結果;Promise
先是弄懂他,再去思考他,最後一步步把功能實現出來,懟他的理解不斷加深,愈來愈透徹;then/catch
方法是最難的,要不停地修修補補;reject
狀態的冒泡是個難題,但在下面的代碼中我沒有專門說起,我也沒有辦法具體說清楚他,我是在整個過程當中不停地調才最終調出來正確的冒泡結果。下面貼代碼,包括整個思考過程,會有點長
爲了說明書寫的邏輯,我使用如下幾個註釋標識,整坨變更的代碼只標識這一坨的開頭處。
//++
——添加的代碼
//-+
——修改的代碼es6
名字隨便取,個人叫MyPromise,沒有取代原生的Promise。github
callback
。當新建 MyPromise
對象時,咱們須要運行此回調,而且 callback
自身也有兩個參數,分別是 resolve
和 reject
,他們也是回調函數的形式;callback
時,若是是 resolve
狀態,將結果保存在 this.__succ_res
中,狀態標記爲成功;若是是 reject
狀態,操做相似;then
方法,是一個原型方法;then
方法時,判斷對象的狀態是成功仍是失敗,分別執行對應的回調,把結果傳入回調處理;...arg
和傳入參數 ...this.__succ_res
都使用了擴展運算符,爲了應對多個參數的狀況,原封不動地傳給 then
方法回調。
callback
回調這裏使用箭頭函數,this
的指向就是本當前MyPromise
對象,因此無需處理this
問題。json
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
能夠簡單實現一些同步代碼,好比:segmentfault
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
中,這兩個例子已經能夠實現了。promise
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方法,即包括上面討論的一、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.resolve()
方法的特性,研究了阮一峯老師的ECMAScript 6 入門對於MyPromise.resolve()
方法的描述部分,得知,這個方法功能很簡單,就是把參數轉換成一個MyPromise
對象,關鍵點在於參數的形式,分別有:
MyPromise
實例;thenable
對象;then
方法的對象,或根本就不是對象;處理的思路是:
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
對象,當他們都成功時,才執行回調。依賴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方法測試
他們倆本質上是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
也是這樣的,我有點不能理解爲啥不捕獲處理它。
這是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狀態屢次改變
到這裏爲止,若是執行下面一段代碼,
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下載