字數:2874
閱讀時間:10分鐘
前言
promise是非同步呼叫的解決方案,它有業界公認的標準:www.ituring.com.cn/article/665… 有許多優秀的實現外掛,如:Jquery、Q等,ES6中也新增了該特性。
它的使用方式也很簡單,如下程式碼:
let promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve('success');
},1000);
});
promise.then(function(response){
console.log(response);
});
//也可以使用defer語法糖,省略建立Promise的過程
let defer = Promise.defer();
setTimeout(function(){
defer.resolve('success');
},1000);
defer.promise.then(function(response){
console.log(response);
})
複製程式碼
但是如果我們想知道更多的內容,如:
1.promise中的執行器引數什麼時候執行的。
2.then函式中的回撥函式什麼時候執行。
3.鏈式呼叫then函式時,它的執行順序是什麼樣的;如果有一個promise被拒絕了,後續如何呼叫;如果程式碼出現異常,後續then函式如何呼叫;如果上一個回撥函式返回了一個promise物件,又是如何執行的...
這時,最好的辦法莫過於檢視實現原始碼了,但更好的辦法是我們自己去實現一次。因此,本文我會與大夥一起基於A+規範實現一個自定義的Promise,旨在更好地理解promise的用法。
正文
先上標準,
中文:www.ituring.com.cn/article/665…
一、實現一個promise類
基於上述標準,理清思路:
1.我們需要實現一個Promise類,其建構函式接受一個執行器引數,並且會在建構函式中呼叫該執行器。
2.Promise應有當前狀態、終值、原因三個屬性。
3.Promise應該實現執行、拒絕兩個動作。
4.由於then是可以多次呼叫的,所以,Promise中應該有儲存執行動作和拒絕動作的兩個佇列。
5.Promise應該暴露一個then函式,用來處理回撥函式。
按照上述思路,我們編寫出如下程式碼:
/**
* 自定義promise物件
*/
class Promise {
/**
* 建構函式
* @param {Function} excutor 執行器
*/
constructor (excutor) {
//promise狀態,有pending、resolved、rejected
this.status = 'pending';
//終值
this.value;
//拒因
this.reason;
//解決函式佇列
this.resolveFuns = [];
//拒絕函式佇列
this.rejectFuns = [];
//解決函式
let resolve = val => {
if (this.status === 'pending') {
this.status = 'resolved';
this.value = val;
this.resolveFuns.forEach(func => func());
}
};
//拒絕函式
let reject = reason => {
if (this.status === 'pending') {
this.status = 'rejected';
this.reason = reason;
this.rejectFuns.forEach(func => func());
}
};
try {
excutor(resolve, reject);
} catch (ex) {
reject(ex);
}
}
/**
* 回撥函式處理
* @param {Function} resolveCallBack 執行回撥函式
* @param {Function} rejectCallBack 拒絕回撥函式
*/
then (resolveCallBack, rejectCallBack) {
}
}
複製程式碼
參照規範:
一個 Promise 的當前狀態必須為以下三種狀態中的一種:等待態(Pending)、執行態(Fulfilled)和拒絕態(Rejected)。
這裡我們status
屬性對應如上三種狀態(我們更習慣使用resolved來表示執行,因此沒有使用fulfilled)。
建構函式中,resolve和reject函式的實現中,遵循了規範中對狀態變化的規定:
等待態(Pending)
處於等待態時,promise 需滿足以下條件:
- 可以遷移至執行態或拒絕態
執行態(Fulfilled)
處於執行態時,promise 需滿足以下條件:
- 不能遷移至其他任何狀態
- 必須擁有一個不可變的終值
拒絕態(Rejected)
處於拒絕態時,promise 需滿足以下條件:
- 不能遷移至其他任何狀態
- 必須擁有一個不可變的據因
如果執行器函式執行報錯,則直接執行拒絕動作。因此,如果我們使用promise時,傳入的執行器執行報錯,則會在then函式中的拒絕回撥函式中捕獲到錯誤。但是,如果是執行器函式中的延時操作報錯(例setTimeout中報錯),這裡是無法捕獲到錯誤的。
二、實現then函式
基於標準,理清思路:
1.then函式接受兩個引數:執行回撥函式和拒絕回撥函式。如果傳入的引數並非函式,則忽略它並將資訊傳遞到下一個then函式中。
2.then函式必須返回一個新的promise。
3.如果promise狀態為pending,則應該將回撥函式推入佇列;如果狀態為resolved,則應直接呼叫執行回撥函式;如果狀態為reject,則應直接呼叫拒絕回撥函式。如果回撥函式執行報錯,則應該執行下一個then函式中的決絕回撥函式。
4.需要處理then的鏈式呼叫。
5.所有的回撥函式都應該非同步執行(為了保證then函式執行順序的一致性。)
首先,我們編寫一個對應引數和返回值的函式,程式碼如下:
/**
* 回撥函式處理
* @param {Function} resolveCallBack 執行回撥函式
* @param {Function} rejectCallBack 拒絕回撥函式
* @return {promise} 新建立的promise
*/
then (resolveCallBack, rejectCallBack) {
let pDeffer = Promise.defer();
if (this.status === 'pending') {
//如果promise狀態為pending則將回撥函式加入佇列中
} else if (this.status === 'resolved') {
//如果promise狀態為resolved,則立即執行resolveCallBack回撥函式,
//如此則無論promise是否已經執行完畢,回撥函式都必然會執行
} else if (this.status === 'rejected') {
//如果promise狀態為rejected,則立即執行rejectedCallBack回撥函式,
//原因同上
}
return pDeffer.promise;
}
複製程式碼
其中defer函式是我在Promise物件上建立的一個靜態函式,是一個建立Promise物件的語法糖函式,其程式碼如下:
/**
* 建立一個預設的promise(語法糖)
* @return {Object} deffer物件
*/
Promise.defer = Promise.deferred = () => {
let defer = {};
defer.promise = new Promise((resolve, reject) => {
defer.resolve = resolve;
defer.reject = reject;
});
return defer;
};
複製程式碼
然後根據整理的思路1中,需要處理引數不為函式的情況。執行回撥函式直接返回接受的返回值即可,拒絕回撥函式丟擲對應原因的異常即可(這裡只有丟擲異常,下一個then函式才會呼叫拒絕回撥函式)。得到如下程式碼:
then (resolveCallBack, rejectCallBack) {
//如果resolveCallBack不是函式,則將值傳遞到下一個resolveCallBack函式中
resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;
//如果rejectCallBack不是函式,則將原因傳遞到下一個rejectCallBack函式中
rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
throw reason;
};
...
}
複製程式碼
然後,我們需要處理回撥函式邏輯。三種狀態的處理邏輯大致一致,我們就以最為複雜的pendding狀態為例。
當狀態為pendding時,我們需要將執行函式和拒絕函式分別推入promise的執行函式佇列和拒絕函式佇列中。該函式必須非同步執行,並且如果執行報錯,則直接執行下一個then的拒絕回撥函式。還有,我們需要考慮終值可能會是一個promise物件的情況,這種情況我們封裝一個函式在第三步進行實現。得到如下程式碼:
/**
* 回撥函式處理
* @param {Function} resolveCallBack 執行回撥函式
* @param {Function} rejectCallBack 拒絕回撥函式
* @return {promise} 新建立的promise
*/
then (resolveCallBack, rejectCallBack) {
//如果resolveCallBack不是函式,則將值傳遞到下一個resolveCallBack函式中
resolveCallBack = Promise.isFunction(resolveCallBack) ? resolveCallBack : x => x;
//如果rejectCallBack不是函式,則將原因傳遞到下一個rejectCallBack函式中
rejectCallBack = Promise.isFunction(rejectCallBack) ? rejectCallBack : reason => {
throw reason;
};
/**
* 解決promise函式
* @param {promise} promise 待解決的promise
* @param {*} x 上一個promise的終值
* @param {Function} resolve promise的執行回撥函式
* @param {Function} reject promise的拒絕回撥函式
*/
function resolvePromise (promise, x, resolve, reject) {
}
let pDeffer = Promise.defer();
if (this.status === 'pending') {
//如果promise狀態為pending則將回撥函式加入佇列中
//新增解決函式佇列
this.resolveFuns.push(() => {
setTimeout(() => {
try {
let x = resolveCallBack(this.value);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
});
//新增拒絕函式佇列
this.rejectFuns.push(() => {
setTimeout(() => {
try {
let x = rejectCallBack(this.reason);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
});
} else if (this.status === 'resolved') {
//如果promise狀態為resolved,則立即執行resolveCallBack回撥函式,
//如此則無論promise是否已經執行完畢,回撥函式都必然會執行
setTimeout(() => {
try {
let x = resolveCallBack(this.value);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
} else if (this.status === 'rejected') {
//如果promise狀態為rejected,則立即執行rejectedCallBack回撥函式,
//原因同上
setTimeout(() => {
try {
let x = rejectCallBack(this.reason);
resolvePromise(pDeffer.promise, x, pDeffer.resolve, pDeffer.reject);
} catch (ex) {
pDeffer.reject(ex);
}
}, 0);
}
return pDeffer.promise;
}
複製程式碼
三、實現解決promise函式
這一步就是為了實現上述的resolvePromise
函式。
整理思路如下:
1.如果終值x與promise是同一個引用,則丟擲異常(死迴圈)。
2.如果x是具有then函式的函式或物件,則遞迴呼叫。由於x是外部不可控變數,所以我們要保證只執行一次執行回撥函式或者拒絕回撥函式。
3.如果x並非具有then函式的函式或物件,則直接呼叫執行回撥函式。
4.如果其中執行報錯,則呼叫拒絕回撥函式。
根據思路,編寫出如下程式碼:
/**
* 解決promise函式
* @param {promise} promise 待解決的promise
* @param {*} x 上一個promise的終值
* @param {Function} resolve promise的執行回撥函式
* @param {Function} reject promise的拒絕回撥函式
*/
function resolvePromise (promise, x, resolve, reject) {
if (x === promise) {
reject(new TypeError('終值與Promise相等,陷入死迴圈!'));
return;
}
let bCalled = false;
if (x != null && (Promise.isFunction(x) || Promise.isObject(x))) {
try {
let then = x.then;
if (Promise.isFunction(then)) {
then.call(x, y => {
if (bCalled === true) {
return;
}
bCalled = true;
resolvePromise(promise, y, resolve, reject);
}, r => {
if (bCalled === true) {
return;
}
bCalled = true;
reject(r);
});
} else {
if (bCalled === true) {
return;
}
bCalled = true;
resolve(x);
}
} catch (ex) {
if (bCalled === true) {
return;
}
bCalled = true;
reject(ex);
}
} else {
if (bCalled === true) {
return;
}
bCalled = true;
resolve(x);
}
}
複製程式碼
這裡有兩個注意點:
1.宣告一個變數接收x.then
變數,是為了避免多次訪問x.then
屬性導致其值在檢索時發生改變。這個可能有點難以理解,我們看一下 promises-aplus-tests 中的測試用例原始碼就更容易理解了:
describe("`x` is an object with normal Object.prototype", function () {
var numberOfTimesThenWasRetrieved = null;
beforeEach(function () {
numberOfTimesThenWasRetrieved = 0;
});
function xFactory() {
return Object.create(Object.prototype, {
then: {
get: function () {
++numberOfTimesThenWasRetrieved;
return function thenMethodForX(onFulfilled) {
onFulfilled();
};
}
}
});
}
testPromiseResolution(xFactory, function (promise, done) {
promise.then(function () {
assert.strictEqual(numberOfTimesThenWasRetrieved, 1);
done();
});
});
});
複製程式碼
如上述程式碼,每次我們呼叫then屬性時,都會導致numberOfTimesThenWasRetrieved
的值加1,從而發生一些不可預期的問題。
2.當x為promise物件時,手動呼叫x的then函式,然後將返回值作為終值傳入promise中,就是為了處理程式碼執行順序的問題。只有當上一then函式中的程式碼執行完畢,才會執行下一個then函式中的回撥函式。因此,我們在使用鏈式呼叫時,上一then函式中執行回撥函式中往往會返回一個promise物件,就是這個原理。
編碼工作到此完畢。我們可以使用promises-aplus-tests外掛跑一把看看結果:
完整程式碼地址:
歡迎關注我的微信公眾號: