async和await實際上是Dart非同步程式設計用於簡化非同步API操作的兩個關鍵字。它的作用就是能夠將非同步的程式碼使用同步的程式碼結構實現。相信學習過之前的Future和Stream的文章就知道對於最終返回的值或者是異常都是採用**非同步回撥方式。**然而async-await就是為了簡化這些非同步回撥的方式,通過語法糖的簡化,將原來非同步回撥方式寫成簡單的同步方式結構。需要注意的是: 使用await關鍵字必須配合async關鍵字一起使用才會起作用。本質上async-await是相當於都Future相關API介面的另一種封裝,提供了一種更加簡便的操作Future相關API的方法。
1. 為什麼需要async-await
通過學習之前非同步程式設計中的Future我們知道,Future一般使用 then
和 catchError
可以很好地處理資料回撥和異常回撥。這實際上還是一種基於非同步回撥的方式,如果非同步操作依賴關係比較複雜需要編寫回撥程式碼比較繁雜,為了簡化這些步驟 async-await
關鍵字通過同步程式碼結構來實現非同步操作,從而使得程式碼更加簡潔和具有可讀性,此外在異常處理方式也會變得更加簡單。
1.1 對比實現程式碼
- Future實現方式
void main() {
_loadUserFromSQL().then((userInfo) {
return _fetchSessionToken(userInfo);
}).then((token) {
return _fetchData(token);
}).then((data){
print('$data');
});
print('main is executed!');
}
class UserInfo {
final String userName;
final String pwd;
bool isValid;
UserInfo(this.userName, this.pwd);
}
//從本地SQL讀取使用者資訊
Future<UserInfo> _loadUserFromSQL() {
return Future.delayed(
Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}
//獲取使用者token
Future<String> _fetchSessionToken(UserInfo userInfo) {
return Future.delayed(Duration(seconds: 2), '3424324sfdsfsdf24324234');
}
//請求資料
Future<String> _fetchData(String token) {
return Future.delayed(
Duration(seconds: 2),
() => token.isNotEmpty
? Future.value('this is data')
: Future.error('this is error'));
}
複製程式碼
輸出結果:
- async-await實現方式
void main() async {//注意:需要新增async,因為await必須在async方法內才有效
var userInfo = await _loadUserFromSQL();
var token = await _fetchSessionToken(userInfo);
var data = await _fetchData(token);
print('$data');
print('main is executed!');
}
class UserInfo {
final String userName;
final String pwd;
bool isValid;
UserInfo(this.userName, this.pwd);
}
//從本地SQL讀取使用者資訊
Future<UserInfo> _loadUserFromSQL() {
return Future.delayed(
Duration(seconds: 2), () => UserInfo('gitchat', '123456'));
}
//獲取使用者token
Future<String> _fetchSessionToken(UserInfo userInfo) {
return Future.delayed(Duration(seconds: 2), () => '3424324sfdsfsdf24324234');
}
//請求資料
Future<String> _fetchData(String token) {
return Future.delayed(
Duration(seconds: 2),
() => token.isNotEmpty
? Future.value('this is data')
: Future.error('this is error'));
}
複製程式碼
輸出結果:
1.2 對比異常下處理
- Future的實現
void main() {
_loadUserFromSQL().then((userInfo) {
return _fetchSessionToken(userInfo);
}).then((token) {
return _fetchData(token);
}).catchError((e) {
print('fetch data is error: $e');
}).whenComplete(() => 'all is done');
print('main is executed!');
}
複製程式碼
- async-await的實現
void main() async {
//注意:需要新增async,因為await必須在async方法內才有效
try {
var userInfo = await _loadUserFromSQL();
var token = await _fetchSessionToken(userInfo);
var data = await _fetchData(token);
print('$data');
} on Exception catch (e) {
print('this is error: $e');
} finally {
print('all is done');
}
print('main is executed!');
}
複製程式碼
通過對比發現使用Future.then相比async-await呼叫鏈路更清晰,基於非同步回撥的方式呼叫相比比較清晰。而在程式碼實現以及同步結構分析async-await顯得更加簡單且處理異常也更加方面,更加符合同步程式碼的邏輯。
2. 什麼是async-await
2.1 async-await基本介紹
async-await本質上是對Future API的簡化形式,將非同步回撥程式碼寫成同步程式碼結構形式。async關鍵字修飾的函式總是返回一個Future物件,所以async並不會阻塞當前執行緒,由前面的EventLoop和Future我們都知道Future的最終會加入EventQueue中,而EventQueue執行是當main函式執行完畢後,才會檢查Microtask Queue和Event Queue並處理它們。await關鍵字意味著中斷當前程式碼執行流程直到當前的async方法執行完畢,如果沒有執行完畢下面的程式碼將處於等待的狀態。但是需要遵循下面兩個規則:
- 要定義非同步函式,請在函式主體之前新增async關鍵字
- **await關鍵字只有在async關鍵字修飾的函式才會有效
**
void main() {
print("executedFuture return ${executedFuture() is Future}");//所以輸出true
print('main is executed!');
}
executedFuture() async {//async函式預設返回的是Future物件
print('executedFuture start');
await Future.delayed(Duration(seconds: 1), () => print('future is finished'));//await等待Future執行完畢
print('Future is executed end');//executedFuture end 輸出必須等待await的Future結束後才會執行
}
複製程式碼
輸出結果: 分析下輸出結果,首先main函式同步執行executedFuture函式和print函式,所以馬上就會同步輸出 “executedFuture start”,但是由於executedFuture是一個async函式,await等待一個Future, 在executedFuture函式作用域內,所以並且在await後面執行,所以需要等待Future資料到了才會執行後面語句,但是此時的executedFuture執行完畢,馬上就執行了main函式中的**“main is executed!”, main執行結束後,就會去檢查MicroTask Queue是否存在需要執行的微任務,如果沒有就會繼續檢查Event Queue是否存在需要處理的Event(其中Future也是一種Event), 所以檢查到executedFuture函式中的Future,就會把它交給EventLoop處理,Future執行完畢後,輸出“future is finished”, 最後執行輸出“Future is executed end”。** ** 如果把上述例子修改一下:
void main() async {//main函式變成一個async函式
await executedFuture(); //executedFuture加入await關鍵字
print('main is executed!');
}
executedFuture() async {
print('executedFuture start');
await Future.delayed(Duration(seconds: 1), () => print('future is finished'));
print('Future is executed end');//executedFuture end 輸出必須等待await的Future結束後才會執行
}
複製程式碼
輸出結果:
分析輸出結果,可能很多人都很疑惑為什麼**“main is executed”**會輸出在最後,其實很容易理解,因為這時候main函式變成了一個async函式,所以必須等待 await executedFuture()
執行完畢後才會執行後面的 print('main is executed!')
. 如果executedFuture沒有執行完畢那麼整個main函式後面程式碼只能等待中,所以一般沒有直接給整個main函式加async關鍵字,這樣會使得整個main函式強行變成了同步執行。
2.2 結合EventLoop理解async-await
通過上面介紹我們知道async-await本質實際上還是Future,本質還是通過向Event Queue中新增Event,然後EventLoop來處理它。但是它們在程式碼結構形式上完全不一樣它是怎麼做到的呢?一起來看下。先給出一個例子:
- Future.then的理解
class DataWrapper {
final String data;
DataWrapper(this.data);
}
Future<DataWrapper> createData() {
return _loadDataFromDisk().then((id) {
return _requestNetworkData(id);
}).then((data) {
return DataWrapper(data);
});
}
Future<String> _loadDataFromDisk() {
return Future.delayed(Duration(seconds: 2), () => '1001');
}
Future<String> _requestNetworkData(String id) {
return Future.delayed(
Duration(seconds: 2), () => 'this is id:$id data from network');
}
複製程式碼
其實通過Future的鏈式呼叫執行邏輯可以把 createData
程式碼按事件進行拆分成下面形式, 可以看到createData函式分為1、2、3:
首先第1塊是當 createData
函式被呼叫後,就同步呼叫執行 _loadDataFromDisk
函式,它返回的是一個Future物件,然後就是等待資料的到達EventLoop, 這樣 _loadDataFromDisk
函式就執行結束了。
然後,當_loadDataFromDisk
函式Future的資料到來時,會觸發第2塊中的 then
函式回撥,拿到id後就立即執行了 _requestNetworkData
函式發出一個HTTP請求,並且先返回一個Future物件,然後就是等待HTTP資料到達EventLoop被處理即可,這樣_requestNetworkData
函式就結束了。
最後,當 _requestNetworkData
函式Future的資料到來時, 第二個 then
函式就被回撥觸發了,然後就是建立 DataWrapper
返回最終資料物件即可。那麼整個createData函式返回的Future最終就返回真實資料,如果外部呼叫者呼叫了這個函式,那麼在它的 then
函式中就能拿到最終的DataWrapper
- async-await的理解
class DataWrapper {
final String data;
DataWrapper(this.data);
}
Future<DataWrapper> createData() async {//createData新增async關鍵字,表示這是一個非同步函式
var id = await _loadDataFromDisk(); //await執行_loadDataFromDisk從磁碟中獲取到id
var data = await _requestNetworkData(id); //通過傳入_loadDataFromDisk的id執行_requestNetworkData返回data
return DataWrapper(data); //最後返回DataWrapper物件
}
Future<String> _loadDataFromDisk() {
return Future.delayed(Duration(seconds: 2), () => '1001');
}
Future<String> _requestNetworkData(String id) {
return Future.delayed(
Duration(seconds: 2), () => 'this is id:$id data from network');
}
複製程式碼
其實通過async-await的類似同步呼叫執行順序邏輯也可以把 createData
程式碼按事件進行拆分成下面形式,只不過之前是通過 then
函式來斷開,這裡通過await關鍵字來斷開分析, 可以看到createData函式分為1、2、3:
首先,當createData函式開始執行就會觸發第一個等待,此時createData就會將自己Future物件返回給呼叫函式,注意:這裡遇到第1個await等待並呼叫_loadDataFromDisk函式的時候,createData函式就會把自己Future物件返回給呼叫函式, 此時的createData函式就已經執行完畢,可能大家比較疑惑沒有顯式看到返回了一個Future物件,這是因為async關鍵字語法糖幫你做了。有的人又會疑惑下面第3步不是返回了嗎?請注意:下面返回的是 DataWrapper
物件不是一個 Future<DataWrapper>
,所以當執行createData函式的時候碰到第1個await等待,就會馬上return一個Future物件,然後createData函式就執行完畢了。觸發第一個等待並且就執行 _loadDataFromDisk
函式,它返回的是一個Future物件, 然後就會一直等待者資料 id
到來。
然後,執行第2塊程式碼,當_loadDataFromDisk
函式,它返回的是一個Future物件中的 id
資料到來時,就會觸發第2個await等待並且呼叫_requestNetworkData
函式,發出HTTP網路請求並且先返回一個Future,然後使用await等待這個HTTP Future的到來。
最後,_requestNetworkData
函式返回Future中的資料到來後,就能拿到HTTP的data資料,最後返回一個DataWrapper, 那麼整個createData函式的Future就拿到最終的資料DataWrapper。
3. 如何使用async-await
3.1 基本使用
通過對比一般同步實現、非同步Future.then的實現、非同步async-await實現,可以更好地理解async-await用法,使用async-await實際上就是相當於把它當作同步程式碼結構形式來寫即可。下面也是以上面例子為例
- 同步實現
假設_loadDataFromDisk和_requestNetworkData函式都是同步執行的,那麼就能很容易寫成它們執行程式碼:
DataWrapper createData() {
var id = _loadDataFromDisk();//同步執行直接return id
var data = _requestNetworkData(id);//同步執行直接return data
return DataWrapper(data);
}
複製程式碼
- 非同步Future.then實現
Future<DataWrapper> createData() {//由於是非同步執行,所以注意返回的物件是一個Future
return _loadDataFromDisk().then((id) {//需要在非同步回撥then函式中拿到id
return _requestNetworkData(id);
}).then((data) {//需要在非同步回撥then函式中拿到data
return DataWrapper(data);//最後返回最終的DataWrapper
});
}
複製程式碼
- 非同步async-await實現
整體程式碼結構形式和同步程式碼結構形式一模一樣只是加了async-await關鍵字以及最後返回的是一個Future物件
Future<DataWrapper> createData() async {//createData新增async關鍵字,表示這是一個非同步函式
//注意: createData遇到第1個await _loadDataFromDisk,就會先返回一個Future<DataWrapper>物件,結束了createData函式。
var id = await _loadDataFromDisk(); //await執行_loadDataFromDisk從磁碟中獲取到id
var data = await _requestNetworkData(id); //通過傳入_loadDataFromDisk的id執行_requestNetworkData返回data
return DataWrapper(data); //最後返回DataWrapper物件
}
複製程式碼
3.2 異常處理
- 同步實現
同步執行程式碼實現異常處理也只能是我們常用的try-catch-finally。
DataWrapper createData() {
try {
var id = _loadDataFromDisk();
var data = _requestNetworkData(id);
return DataWrapper(data);
} on Exception catch (e) {
print('this is error');
} finally {
print('executed done');
}
}
複製程式碼
- 非同步Future.then實現
Future.then實現非同步異常的捕獲一般是藉助 catchError
來實現的。
Future<DataWrapper> createData() {
return _loadDataFromDisk().then((id) {
return _requestNetworkData(id);
}).then((data) {
return DataWrapper(data);
}).catchError((e) {//catchError捕獲異常
print('this is error: $e');
}).whenComplete((){
print('executed is done');
});
}
複製程式碼
- 非同步async-await實現
async-await實現非同步異常的捕獲和同步實現一模一樣。
Future<DataWrapper> createData() async {
try {
var id = await _loadDataFromDisk();
var data = await _requestNetworkData(id);
return DataWrapper(data);
} on Exception catch (e) {
print('this is error: $e');
} finally {
print('executed is done');
}
}
複製程式碼
4. async-await使用場景
其實關於async-await的使用場景, 學完上面的內容基本上都可以總結分析出來。
- 大部分Future使用的場景都可以使用async-await來替代,也建議使用async-await,畢竟人家這倆關鍵字就是為了簡化Future API的呼叫,而且能將非同步程式碼寫成同步程式碼形式。程式碼簡潔度上也能得到提升。
- 其實通過上面例子也能明顯發現,對於一些依賴關係比較明顯的Future,建議還是使用Future,畢竟鏈式呼叫功能非常強大,可以一眼就能看到每個Future之間的前後依賴關係。因為通過async-await雖然簡化了回撥形式,但是在某種程度上降低了Future之間的依賴關係。
- 對於依賴關係不明顯且彼此獨立Future,可以使用async-await。
5. 熊喵先生的小總結
到這裡有關Dart非同步程式設計中的async-await使用到這裡就結束了,實際上async-await就是一個語法糖的使用,本質還是對Future API的使用,它的好處和優勢就是可以將非同步執行寫成同步程式碼結構形式,我們也對它的程式碼結構進行拆分分析,清晰地分析了其語法糖背後的原理。這樣對使用async-await非常有幫助。
感謝關注,熊喵先生願和你在技術路上一起成長!