本文大多數內容翻譯自該篇文章
1.什麼是Promise
Promise可以認為是一種用來解決非同步處理的程式碼規範。常見的非同步處理是使用回撥函式,回撥函式有兩種模式,同步的回撥和非同步的回撥。一般回撥函式指的是非同步的回撥。
同步回撥
function add(a, b, callback) { callback(a + b) }
console.log('before');
add(1, 2, result => console.log('Result: ' + result);
console.log('after');
複製程式碼
輸出結果為: before Result:3 after
非同步回撥
function addAsync(a, b, callback) {
setTimeout( () => callback(a + b), 1000);
}
console.log('before');
addAsync(1, 2, result => console.log('Result: ' + result));
console.log('after');
複製程式碼
輸出結果: before after Result: 3
然而回撥函式有個著名的坑就是“callback hell”,比如:
doSomething1(function(value1) {
doSomething2(function(value2) {
doSomething3(function(value3) {
console.log("done! The values are: " + [value1, value2, value3].join(','));
})
})
})
複製程式碼
為了等value1, value2, value3資料都準備好,必須要一層一層巢狀回撥函式。如果一直巢狀下去,就形成了callback hell,不利於程式碼的閱讀。
如果改用Promise的寫法,只要寫成如下方式就行。
doSomething1().then(function() {
return value1;
}).then(function(tempValue1) {
return [tempValue1, value2].join(',');
}).then(function(tempValue2) {
console.log("done! ", [tempValue2, value3].join(','));
});
複製程式碼
可以注意到,Promise實際上是把回撥函式從doSomething
函式中提取到了後面的then
方法裡面,從而防止多重巢狀的問題。
一個 Promise 物件代表一個目前還不可用,但是在未來的某個時間點可以被解析的值。它要麼解析成功,要麼失敗丟擲異常。它允許你以一種同步的方式編寫非同步程式碼。
Promise的實現是根據Promises/A+規範實現的。
2.Promise物件和狀態
對於Promise的基本使用和入門,可以參考promise-book。這裡對Promise的使用做了比較詳細的介紹。
2.1 resolve & reject
Promise建構函式用來構造一個Promise物件,其中入參匿名函式中resolve
和reject
這兩個也都是函式。如果resolve
執行了,則觸發promise.then中成功的回撥函式;如果reject
執行了,則觸發promise.then中拒絕的回撥函式。
var promise = new Promise(function(resolve, reject) {
// IF 如果符合預期條件,呼叫resolve
resolve('success');
// ELSE 如果不符合預期條件,呼叫reject
reject('failure')
})
複製程式碼
2.2 Fulfilled & Rejected
Promise物件一開始的值是Pending準備狀態。
執行了resolve()
後,該Promise物件的狀態值變為onFulfilled狀態。
執行了reject()
後,該Promise物件的狀態值變為onRejected狀態。
Promise物件的狀態值一旦確定(onFulfilled或onRejected),就不會再改變。即不會從onFulfilled轉為onRejected,或者從onRejected轉為onFulfilled。
2.3 快捷方法
獲取一個onFulfilled狀態的Promise物件:
Promise.resolve(1);
// 等價於
new Promise((resolve) => resolve(1));
複製程式碼
獲取一個onRejected狀態的Promise物件:
Promise.reject(new Error("BOOM"))
// 等價於
new Promise((resolve, reject)
=> reject(new Error("BOOM")));
複製程式碼
更多快捷方法請參考Promise API。
3.異常捕獲:then和catch
Promise的異常捕獲有兩種方式:
then
匿名函式中的reject
方法catch
方法
3.1 then中的reject方法捕獲異常
這種方法只能捕獲前一個Promise物件中的異常,即呼叫then
函式的Promise物件中出現的異常。
var promise = Promise.resolve();
promise.then(function() {
throw new Error("BOOM!")
}).then(function (success) {
console.log(success);
}, function (error) {
// 捕捉的是第一個then返回的Promise物件的錯誤
console.log(error);
});
複製程式碼
但該種方法無法捕捉當前Promise物件的異常,如:
var promise = Promise.resolve();
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}, function (error) {
console.log(error); // 無法捕捉當前then中丟擲的異常
});
複製程式碼
3.2 catch捕獲異常
上述栗子若改寫成如下形式,最後追加一個catch函式,則可以正常捕捉到異常。
var promise = Promise.resolve();
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).catch(function (error) {
console.log(error); // 可以正常捕捉到異常
});
複製程式碼
catch
方法可以捕獲到then
中丟擲的錯誤,也能捕獲前面Promise丟擲的錯誤。 因此建議都通過catch
方法捕捉異常。
var promise = Promise.reject("BOOM!");
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).catch(function (error) {
console.log(error); // BOOM!
});
複製程式碼
值得注意的是:catch
方法其實等價於then(null, reject)
,上面可以寫成:
promise.then(function() {
return 'success';
}).then(function (success) {
console.log(success);
throw new Error("Another BOOM!");
}).then(null, function(error) {
console.log(error);
})
複製程式碼
總結來說就是:
-
使用
promise.then(onFulfilled, onRejected)
的話,在onFulfilled
中發生異常的話,在onRejected
中是捕獲不到這個異常的。 -
在
promise.then(onFulfilled).catch(onRejected)
的情況下then
中產生的異常能在.catch
中捕獲 -
.then
和.catch
在本質上是沒有區別的需要分場合使用。
4.動手逐步實現Promise
瞭解一個東西最好的方式就是嘗試自己實現它,儘管可能很多地方不完整,但對理解內在的執行原理是很有幫助的。
這裡主要引用了JavaScript Promises ... In Wicked Detail這篇文章的實現,以下內容主要是對該篇文章的翻譯。
4.1 初步實現
首先實現一個簡單的Promise物件型別。只包含最基本的then
方法和resolve
方法,reject
方法暫時不考慮。
function Promise(fn) {
// 設定回撥函式
var callback = null;
// 設定then方法
this.then = function (cb) {
callback = cb;
};
// 定義resolve方法
function resolve(value) {
// 這裡強制resolve的執行在下一個Event Loop中執行
// 即在呼叫了then方法後設定完callback函式,不然callback為null
setTimeout(function () {
callback(value);
}, 1);
}
// 執行new Promise時傳入的函式,入參是resolve
// 按照之前講述的,傳入的匿名函式有兩個方法,resolve和reject
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
// 呼叫自己的Promise
doSomething().then(function (value) {
console.log("got a value", value);
});
複製程式碼
好了,這是一個很粗略版的Promise。這個實現連Promise需要的三種狀態都還沒實現。這個版本主要直觀展示了Promise的核心方法:then
和resolve
。
該版本如果then
非同步呼叫的話,還是會導致Promise中的callback為null。
var promise = doSomething();
setTimeout(function() {
promise.then(function(value) {
console.log("got a value", value);
})}, 1);
複製程式碼
後續通過加入狀態來維護Promise,就可以解決這種問題。
4.2 Promise新增狀態
通過新增一個欄位state
用來維護Promise的狀態,當執行了resolve
函式後,修改state
為resolved
,初始state
是pendding
。
function Promise(fn) {
var state = 'pending'; // 維護Promise例項的狀態
var value;
var deferred; // 在狀態還處於pending時用於儲存回撥函式的引用
function resolve(newValue) {
value = newValue;
state = 'resolved';
if (deferred) {
// deferred 有值表明回撥已經設定了,呼叫handle方法處理回撥函式
handle(deferred);
}
}
// handle方法通過判斷state選擇如何執行回撥函式
function handle(onResolved) {
// 如果還處於pending狀態,則先儲存then傳入的回撥函式
if (state === 'pending') {
deferred = onResolved;
return;
}
onResolved(value);
}
this.then = function (onResolved) {
// 對then傳入的回撥函式,呼叫handle去執行回撥函式
handle(onResolved);
};
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
doSomething().then(function (value) {
console.log("got a value", value);
});
複製程式碼
加入了狀態後,可以通過判斷狀態來解決呼叫先後順序的問題:
-
在
resolve()
執行前呼叫then()
。表明這時還沒有value處理好,這時的狀態就是pending
,此時先保留then()
傳入的回撥函式,等呼叫resolve()
處理好value值後再執行回撥函式,此時回撥函式儲存在deferred
中。 -
在
resolve()
執行後呼叫then()
。表明這時value已經通過resolve()
處理完成了。當呼叫then()
時就可以通過呼叫傳入的回撥函式處理value值。
該版本的Promise我們可以隨意先呼叫resolve()
或pending()
,兩者的順序對程式的執行不會造成影響了。
4.3 Promise新增呼叫鏈
Promise是可以鏈式呼叫的,每次呼叫then()
後都返回一個新的Promise例項,因此要修改之前實現的then()
方法。
function Promise(fn) {
var state = 'pending';
var value;
var deferred = null;
function resolve(newValue) {
value = newValue;
state = 'resolved';
if (deferred) {
handle(deferred);
}
}
// 此時傳入的引數是一個物件
function handle(handler) {
if (state === 'pending') {
deferred = handler;
return;
}
// 如果then沒有傳入回撥函式
// 則直接執行resolve解析value值
if (!handler.onResolved) {
handler.resolve(value);
return;
}
// 獲取前一個then回撥函式中的解析值
var ret = handler.onResolved(value);
handler.resolve(ret);
}
// 返回一個新的Promise例項
// 該例項匿名函式中執行handle方法,該方法傳入一個物件
// 包含了傳入的回撥函式和resolve方法的引用
this.then = function (onResolved) {
return new Promise(function (resolve) {
handle({
onResolved: onResolved, // 引用上一個Promise例項then傳入的回撥
resolve: resolve
});
});
};
fn(resolve);
}
function doSomething() {
return new Promise(function (resolve) {
var value = 42;
resolve(value);
});
}
// 第一個then的返回值作為第二個then匿名函式的入參
doSomething().then(function (firstResult) {
console.log("first result", firstResult);
return 88;
}).then(function (secondResult) {
console.log("second result", secondResult);
});
複製程式碼
then
中是否傳入回撥函式也是可選的,如:
doSomething().then().then(function(result) {
console.log('got a result', result);
});
複製程式碼
在handle()
方法的實現中,如果沒有回撥函式,直接解析已有的value值,該值是上一個Promise例項中呼叫resolve(value)
中傳入的。
if(!handler.onResolved) {
handler.resolve(value);
return;
}
複製程式碼
如果回撥函式中返回的是一個Promise物件而不是一個具體數值怎麼辦?此時我們需要對返回的Promise呼叫then()
方法。
doSomething().then(function(result) {
// doSomethingElse returns a promise
return doSomethingElse(result);
}).then(function(anotherPromise) {
anotherPromise.then(function(finalResult) {
console.log("the final result is", finalResult);
});
});
複製程式碼
每次這樣寫很麻煩,我們可以在我們的Promise中的resole()
方法內處理掉這種情況。
function resolve(newValue) {
// 通過判斷是否有then方法判斷其是否是Promise物件
if (newValue && typeof newValue.then === 'function') {
// 遞迴執行resolve方法直至解析出值出來,
// 通過handler.onResolved(value)解析出值,這裡handler.onResolve就是resolve方法
newValue.then(resolve);
return;
}
state = 'resolved';
value = newValue;
if (deferred) {
handle(deferred);
}
}
複製程式碼
4.4 Promise新增reject處理
直至目前為止,已經有了一個比較像樣的Promise了,現在新增一開始忽略的reject()
方法,使得我們可以這樣使用Promise。
doSomething().then(function(value) {
console.log('Success!', value);
}, function(error) {
console.log('Uh oh', error);
});
複製程式碼
實現也很簡單,reject()
方法與resolve()
方法類似。
function Promise(fn) {
var state = 'pending';
var value;
var deferred = null;
function resolve(newValue) {
if (newValue && typeof newValue.then === 'function') {
newValue.then(resolve, reject);
return;
}
state = 'resolved';
value = newValue;
if (deferred) {
handle(deferred);
}
}
// 新增的reject方法,這裡將Promise例項的狀態設為rejected
function reject(reason) {
state = 'rejected';
value = reason;
if (deferred) {
handle(deferred);
}
}
function handle(handler) {
if (state === 'pending') {
deferred = handler;
return;
}
var handlerCallback;
// 新增state對於rejected狀態的判斷
if (state === 'resolved') {
handlerCallback = handler.onResolved;
} else {
handlerCallback = handler.onRejected;
}
if (!handlerCallback) {
if (state === 'resolved') {
handler.resolve(value);
} else {
handler.reject(value);
}
return;
}
var ret = handlerCallback(value);
handler.resolve(ret);
}
this.then = function (onResolved, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onResolved: onResolved,
onRejected: onRejected,
resolve: resolve,
reject: reject
});
});
};
fn(resolve, reject);
}
function doSomething() {
return new Promise(function (resolve, reject) {
var reason = "uh oh, something bad happened";
reject(reason);
});
}
// 呼叫栗子
doSomething().then(function (firstResult) {
// wont get in here
console.log("first result:", firstResult);
}, function (error) {
console.log("got an error:", error);
});
複製程式碼
目前我們的異常處理機制只能處理自己丟擲的異常資訊,對於其他的一些異常資訊是無法正常捕獲的,如在resolve()
方法中丟擲的異常。我們對此做如下修改:
function resolve(newValue) {
try {
// ... as before
} catch(e) {
reject(e);
}
}
複製程式碼
這裡通過新增try catch
手動捕獲可能出現的異常,並在catch
中呼叫reject()
方法進行處理。同樣對於回撥函式,執行時也可能出現異常,也需要做同樣的處理。
function handle(deferred) {
// ... as before
var ret;
try {
ret = handlerCallback(value);
} catch(e) {
handler.reject(e);
return;
}
handler.resolve(ret);
}
複製程式碼
上述完整的演示程式碼請檢視原文作者提供的fiddle。
4.4 Promise保證非同步處理
到目前為止,我們的Promise已經實現了基本比較完善的功能了。這裡還有一點需要注意的是,Promise規範提出不管是resolve()
還是reject()
,執行都必須保持非同步處理。要實現這一點很簡單,只需做如下修改即可:
function handle(handler) {
if(state === 'pending') {
deferred = handler;
return;
}
setTimeout(function() {
// ... as before
}, 1);
}
複製程式碼
問題是為什麼要這麼處理?這主要是為了保證程式碼執行流程的一致性和可靠性。考慮如下栗子:
var promise = doAnOperation();
invokeSomething();
promise.then(wrapItAllUp);
invokeSomethingElse();
複製程式碼
通過程式碼的意圖應該是希望invokeSomething()
和invokeSomethingElse()
都執行完後,再執行回撥函式wrapItAllUp()
。如果Promise的resolve()
處理不是非同步的話,則執行順序變為invokeSomething()
-> wrapItAllUp()
-> invokeSomethingElse()
,跟預想的產生不一致。
為了保證這種執行順序的一致性,Promise規範要求resolve
必須是非同步處理的。
到這一步,我們的Promise基本像模像樣了。當然離真正的Promise還有一段差距,比如缺乏了常用的便捷方法如all()
,race()
等。不過本例子實現的方法本來就是從理解Promise原理出發的,相信通過該例子對Promise原理會有比較深入的瞭解。
參考
- JavaScript Promises ... In Wicked Detail。
- Promises/A+。
- promise-book
- A quick guide to JavaScript Promises
- MDN web docs
- Node.js Design Patterns