總目錄
從C#到TypeScript - Promise
背景
相信之前用過JavaScript的朋友都碰到過異步回調地獄(callback hell),N多個回調的嵌套不僅讓代碼讀起來十分困難,維護起來也很不方便。
其實C#在Task
出現之前也是有類似場景的,Async Programming Mode時代,用Action
和Func
做回調也很流行,不過也是意識到太多的回調嵌套代碼可讀性差且維護不易,微軟引入了Task
和Task-based Async Pattern。
雖然不知道是哪個語言最早有這個概念,但相信是C#把async await
帶到流行語言的舞台,接着其他語言也以不同的形式支持async await
,如Python, Dart, Swift等。
JavaScript同樣在ES6開始支持Promise
和Generator
,並在ES7中提出支持async await
的議案。
這篇先來看看Promise:
Promise的特點
Promise
之於TypeScript,相當於Task
之於C#,只有返回Promise
的函數才能使用async await
。
Promise
其實就是一個可以獲取異步結果,並封裝了一些異步操作的對象。
有三個狀態:
pending
: 進行中
resolved
: 成功
rejected
: 失敗
並且這三個狀態只有兩種轉換:pending
->resolved
、pending
->rejected
,不是成功就是失敗,並沒有多余的狀態轉換。
這兩種轉換都是由異步返回的結果給定的,成功取回數據就是resolved
,取數據出異常就是rejected
。
也因此,這轉換過后的結果就是固定的了,不可能在轉換過后還會變回pending
或其他狀態。
Promise
不能在任務進行中取消,只能等結果返回,這點上不如C#的Task
,Task
可以通過CancelTaskToken
來取消任務。
Promise的使用
可以直接new一個Promise
對象,構造函數的參數是一個有兩個參數的函數。
這兩個參數一個是resove
,用來在異步操作成功后調用,並把異步結果傳出去,調用resove
后狀態就由pending
->resolved
。
另一個是reject
,用來在失敗或異常時調用,並把錯誤消息傳出去,調用reject
后狀態由pending
->rejected
。
var promise = new Promise(function(resolve, reject) {
});
通常需要在成功或失敗后做一些操作,這時需要then
來做這個事,then
可以有兩個函數參數,第一個是成功后調用的,第二個是失敗調用的,第二個是可選的。
另外,then
返回的也是一個Promise,不過不是原來的那個,而是新new出來的,這樣可以鏈式調用,then
后面再接then
。
// 函數參數用lambda表達式寫更簡潔
promise.then(success => {
console.info(success);
}, error => {
console.info(error);
}).then(()=>console.info('finish'));
嵌套的Promise
在實際場景中,我們可能需要在一個異步操作后再接個異步操作,這樣就會有Promise
的嵌套操作。
下面的代碼顯示的是Promise
的嵌套操作:
p1
先打印"start",延時兩秒打印"p1"。
p2
在p1
完成后延時兩秒打印"p2"。
function delay(): Promise<void>{
return new Promise<void>((resolve, reject)=>{setTimeout(()=>resolve(), 2000)});
}
let p1 = new Promise((resolve, reject) => {
console.info('start');
delay().then(()=>{
console.info('p1');
resolve()
});
});
let p2 = new Promise((resolve, reject) => {
p1.then(()=>delay().then(()=>resolve()));
});
p2.then(()=>console.info('p2'));
異常處理
上面提到Promise
出錯時把狀態變為rejected
並把錯誤消息傳給reject
函數,在then
里面調用reject
函數就可以顯示異常。
不過這樣寫顯得不是很友好,Promise
還有個catch
函數專門用來處理錯誤異常。
而且Promise
的異常是冒泡傳遞的,最后面寫一個catch
就可以捕獲到前面所有promise可能發生的異常,如果用reject
就需要每個都寫。
所以reject
函數一般就不需要在then
里面寫,在后面跟個catch
就可以了。
new Promise(function(resolve, reject) {
throw new Error('error');
}).catch(function(error) {
console.info(error); // Error: error
});
也如上面所說狀態只有兩種變化且一旦變化就固定下來,所以如果已經在Promise
里執行了resolve
,再throw異常是沒用的,catch不到,因為狀態已經變成resolved
。
new Promise(function(resolve, reject) {
resolve('success');
throw new Error('error');
}).catch(function(error) {
console.info(error); // 不會執行到這里
});
另外,catch
里的代碼也可能出異常,所以catch
后面也還可以跟catch
的議案。
new Promise(function(resolve, reject) {
throw new Error('error');
}).catch(function(error) {
console.info(error); // Error: error
throw new Error('catch error');
}).catch(function(error){
console.info(error); // Error: catch error
};
BlueBird的 finally 和 done
異常的try...catch
后面可以跟finally
來執行必須要執行的代碼,Promise
原生並不支持,可以引入BlueBird的擴展庫來支持。
另外還有done
在最后面來表示執行結束並拋出可能出現的異常,比如最后一個catch
代碼塊里的異常。
let p = new Promise(function(resolve, reject) {
x = 2; // error, 沒有聲明x變量
resolve('success');
}).catch(function(error) {
console.info(error);
}).finally(()=>{ // 總會執行這里
console.info('finish');
y = 2; // error, 沒有聲明y變量
}).done();
try{
p.then(()=>console.info('done'));
} catch (e){
console.info(e); // 由於最后面的done,所以會把finally里的異常拋出來,如果沒有done則不會執行到這里
}
並行執行Promise
雖然JavaScript是單線程語言,但並不妨礙它執行一些IO並行操作,如不阻塞發出http request,然后異步等待。
Promise
除了用then
來順序執行外,也同樣可以不阻塞同時執行多個Promise
然后等所有結果返回再進行后續操作。
C#的Task
有個WhenAll
的靜態方法來做這個事,Promise
則是用all
方法達到同樣目的。
all
方法接受實現Iterator接口的對象,比如數組。
let p = Promise.all([p1, p2, p3]);
all
返回的是一個新的Promise
- p,p的狀態是由p1, p2, p3同時決定的:
p.resolved = p1.resolve && p2.resolve && p3.resolve
p.rejected = p1.rejected || p2.rejected || p3.rejected
也就是說p的成功需要p1,p2,p3都成功,而只要p1, p2, p3里有任何一個失敗則p失敗並退出。
Promise
還有一個方法race
同樣是並行執行多個Promise
,不同於all
的是它的成功狀態和錯誤狀態一樣,只要有一個成功就成功,如同C# Task的Any
方法。
let p = Promise.race([p1, p2, p3]);