十一:Flutter之Dart異步操作?
1: Dart 的異步模型?
我們先搞清楚dart是如何搞定異步操作的?
1.1: Dart 是單線程的
1.1.1: 程序中的耗時操作
開發(fā)中的耗時操作:
- 在開發(fā)中辕羽、我們經(jīng)常會遇到一些耗時操作需要完成彻磁、比如網(wǎng)絡(luò)請求纬纪、文件讀取等等
- 如果我們的主線程一直在等待這些耗時操作然爆、那么就會進行阻塞绎秒、無法響應(yīng)其他事件辕漂、比如用戶的點擊呢灶;
- 顯然、我們不能這樣做钉嘹。
如何處理耗時的操作
針對如何處理耗時的操作鸯乃、不同的語言有不同的處理方式
- 處理方式一:多線程、比如Java跋涣、C++缨睡、我們普遍使用的做法就是開啟另外一條新的線程(Thread)鸟悴、在新的線程中完成這些異步的操作、再通過線程間通信的方式宏蛉、將拿到的數(shù)據(jù)傳遞給主線程遣臼。
- 處理方式二:單線程 + 事件循環(huán),比如JavaScript拾并、Dart就是基于單線程加事件循環(huán)來完成耗時操作的處理揍堰、不過單線程如何能進行耗時的操作。
1.1.2: 單線程的異步操作
之前很多開發(fā)者都對于單線程的異步操作很疑惑嗅义,其實它們并不沖突:
- 因為我們的一個應(yīng)用程序大部分都是處于空閑的狀態(tài)屏歹、并不是無限制的在和用戶進行交互。
- 比如等待用戶的點擊之碗、網(wǎng)絡(luò)請求數(shù)據(jù)的返回蝙眶、文件讀取的IO操作、這些等待的行為并不會阻塞我們的線程褪那。
- 這是因為類似于網(wǎng)絡(luò)請求幽纷、文件讀取IO、我們都可以基于非阻塞式調(diào)用
阻塞式調(diào)用和非阻塞式的調(diào)用
如果想搞清楚這個點博敬、我們需要知道操作系統(tǒng)中的阻塞式調(diào)用和非阻塞式調(diào)用的概念友浸。
- 阻塞式和非阻塞式關(guān)注的是程序在等待調(diào)用結(jié)果(消息、返回值)時的狀態(tài)偏窝。
- 阻塞式調(diào)用:調(diào)用結(jié)果返回之前收恢、當(dāng)前線程會被掛起、調(diào)用線程只有在得到調(diào)用結(jié)果之后才會繼續(xù)執(zhí)行祭往。
- 非阻塞式調(diào)用:調(diào)用執(zhí)行之后伦意、當(dāng)前線程不會停止執(zhí)行、只需要過一段時間來檢查一下有沒有結(jié)果返回即可硼补。
我們開發(fā)中很多的耗時操作驮肉、都可以基于這樣的非阻塞式調(diào)用。
- 比如網(wǎng)絡(luò)請求本身使用socket通信已骇、而Socket本身提供了select模型离钝、可以進行非阻塞式的工作。
- 比如文件讀取IO操作疾捍、我們可以使用操作系統(tǒng)提供的基于事件的回調(diào)機制奈辰。
這些操作都不會阻塞我們單線程的繼續(xù)執(zhí)行、我們的線程在等待的過程中乱豆、可以繼續(xù)去做別的事情奖恰、等真正有了響應(yīng)、再去進行對應(yīng)的處理即可。這時瑟啃、我們可能有兩個問題:
- 問題1: 如果在多核CPU中论泛、單線程是不是就沒有充分利用CPU呢、這個問題后續(xù)補充說明蛹屿。
- 問題2: 單線程是如何處理網(wǎng)絡(luò)通信屁奏、IO操作它們返回的結(jié)果呢、答案就是事件循環(huán)(Event Loop)错负。
1.2: Dart事件循環(huán)
1.2.1: 什么是事件循環(huán)
單線程模型中坟瓢、主要就是在維護著一個事件循環(huán)(Event Loop)
事件循環(huán)是什么呢?
- 事實上事件循環(huán)就是將需要處理的一系列事件(包括點擊事件犹撒、IO事件折联、網(wǎng)絡(luò)事件)放在一個事件隊列中(Event Queue)中。
- 不斷地從事件隊列(Event Queue)中取出事件识颊、并執(zhí)行其對應(yīng)需要執(zhí)行的代碼塊诚镰、直到事件隊列清空為止。
下面就是一段時間小循環(huán)的偽代碼如下:
// 事件循環(huán)的偽代碼
List eventQueue = [];
var event;
while (true) {
if (eventQueue.length > 0) {
// 取出一個事件
event = eventQueue.removeAt(0);
// 執(zhí)行該事件
event();
}
}
當(dāng)我們有一些事件祥款、比如點擊事件清笨、IO事件、網(wǎng)絡(luò)事件時刃跛、就會加入到EventLoop中抠艾、當(dāng)發(fā)現(xiàn)事件隊列中不為空時、就會立即取出事件奠伪、并且執(zhí)行事件跌帐。
齒輪??就是我們的事件循環(huán)首懈、它會從隊列中一次取出事件來執(zhí)行绊率。1.2.2: 事件循環(huán)代碼的模擬
這里我們舉例一段Flutter的代碼:
RaisedButton (
child: Text('click me'),
onPressed: () {
final myFeature = http.get('https://www.baidu.com');
myFeature.then((response) {
if (response.statusCode == 200) {
print('Success!');
}
});
},
)
- 一個按鈕RaisedButton、當(dāng)發(fā)生點擊時執(zhí)行onPressed函數(shù)究履。
- onPressed函數(shù)中滤否、我們發(fā)送了一個網(wǎng)絡(luò)請求、請求成功后會執(zhí)行then中的函數(shù)最仑。
這些代碼是如何放在事件循環(huán)中執(zhí)行的呢藐俺?
- 1: 當(dāng)用戶發(fā)生點擊的時候、onPressed回調(diào)函數(shù)被放入事件循環(huán)中執(zhí)行泥彤、執(zhí)行的過程中發(fā)送了一個網(wǎng)絡(luò)請求欲芹。
- 2: 網(wǎng)絡(luò)請求發(fā)出去后、該事件循環(huán)不會被阻塞吟吝、而是發(fā)現(xiàn)要執(zhí)行的onPressed函數(shù)已經(jīng)結(jié)束菱父、會將它丟棄臣疑。
- 3: 網(wǎng)絡(luò)請求成功后、會執(zhí)行then中傳入的回調(diào)函數(shù)扁远、這也是一個事件酬荞、該事件會被放入到事件循環(huán)中執(zhí)行、執(zhí)行完畢后粟瞬、事件循環(huán)將其丟棄同仆。
盡管onPressed 和 then中的回調(diào)有一些差異、但是它們對于事件循環(huán)來說裙品、都是告訴它:我有一段代碼需要執(zhí)行俗批、快點幫我完成它。2: Dart的異步操作
Dart 中的異步操作主要使用Future市怎、async扶镀、await
如果你之前接觸過前端的ES6、ES7焰轻、編程經(jīng)驗臭觉,那么完全可以將Feature理解成Promise、(async辱志、await)和ES7中基本一致蝠筑。2.1: 認(rèn)識Feature
2.1.1: 同步的網(wǎng)絡(luò)請求
我們先來看一個例子吧:
- 在這個例子中、我使用getNetworkData來模擬了一個網(wǎng)絡(luò)請求揩懒;
- 該網(wǎng)絡(luò)請求在需要3秒鐘的時間什乙、之后返回數(shù)據(jù);
import 'dart:io';
main(List<String> args) {
/* 代碼執(zhí)行結(jié)果:
main function start
netWorkData
null
main function end
*/
print('main function start');
print(getNetWorkData());
print('main function end');
}
String getNetWorkData() {
sleep(Duration(seconds: 3));
print('netWorkData');
}
上面代碼執(zhí)行結(jié)果:
main function start
/// 等待3秒
netWorkData
null
main function end
顯然已球、上面的代碼不是我們想要的執(zhí)行效果臣镣、因為網(wǎng)絡(luò)請求阻塞了main函數(shù)、那么意味著其后所有的代碼都無法正常的繼續(xù)執(zhí)行智亮。
2.1.2: 異步的網(wǎng)絡(luò)請求
我們對上面的同步執(zhí)行代碼進行改進
- 和剛才的代碼唯一的區(qū)別在于我使用Future對象來將耗時操作放在了其中傳入的函數(shù)中忆某;
- 稍后、我們會說明它具體的一些API阔蛉、我們就暫時知道我創(chuàng)建了一個Future對象實例即可弃舒。
import 'dart:io';
main(List<String> args) {
print('main function start');
// 使用變量來接收getNetWorkData返回的future對象
var future = getNetWorkData();
// 當(dāng)future實例有返回結(jié)果時、會自動回調(diào)then中傳入的函數(shù)
// 該函數(shù)會放入到事件循環(huán)中状原、被執(zhí)行聋呢。
future.then((value) {
// 獲取返回成功的回調(diào)
/// value=newWorkData
print('value---------------$value');
}).catchError((error) {
// 捕獲出現(xiàn)的異常error
print('error---------$error');
});
print('future----------------$future');
print(getNetWorkData());
print('main function end');
/* 第1次打印結(jié)果:
main function start
Instance of 'Future<String>'
main function end
*/
// 第2次success打印結(jié)果:
/*
main function start
future----------------Instance of 'Future<String>'
Instance of 'Future<String>'
main function end
value---------------newWorkData
*/
// 第3次failure打印結(jié)果:
/*
main function start
future----------------Instance of 'Future<String>'
Instance of 'Future<String>'
main function end
error---------Exception: 網(wǎng)絡(luò)請求出錯
Unhandled exception:
Exception: 網(wǎng)絡(luò)請求出錯
#0 getNetWorkData.<anonymous closure> (file:///Users/qxu7859/Desktop/Dart%E7%BB%83%E4%B9%A0/7_Dart_%E5%BC%82%E6%AD%A5%E6%93%8D%E4%BD%9C.dart:99:5)
#1 new Future.<anonymous closure> (dart:async/future.dart:176:37)
#2 Timer._createTimer.<anonymous closure> (dart:async-patch/timer_patch.dart:21:15)
#3 _Timer._runTimers (dart:isolate-patch/timer_impl.dart:382:19)
#4 _Timer._handleMessage (dart:isolate-patch/timer_impl.dart:416:5)
#5 _RawReceivePortImpl._handleMessage (dart:isolate-patch/isolate_patch.dart:172:12)
*/
}
Future<String> getNetWorkData() {
return Future<String>(() {
sleep(Duration(seconds: 3));
/// 返回成功結(jié)果
//return 'newWorkData';
/// 返回異常
throw Exception('網(wǎng)絡(luò)請求出錯');
});
}
2.1.3: Future使用補充
補充1: 上面案例的總結(jié):
我們通過一個案例來學(xué)習(xí)一個future的使用過程:
- 1: 創(chuàng)建一個Future對象(可能是我們創(chuàng)建的、也可能是調(diào)用內(nèi)部API或者第三方API獲取到的一個Future)颠区、總之你需要獲取一個Future實例削锰。
- 2: 通過.then成功回調(diào)函數(shù)的方式來監(jiān)聽Future內(nèi)部執(zhí)行完成時獲取的結(jié)果
- 3: 通過.catchError(失敗或異常回調(diào)函數(shù)的方式來監(jiān)聽Future內(nèi)部執(zhí)行失敗或者出現(xiàn)異常時的錯誤信息)
補充2: Future的兩種狀態(tài)
事實上Future在執(zhí)行的整個過程中毕莱、我們通常把它分成兩種狀態(tài):未完成狀態(tài) 和 完成狀態(tài)
狀態(tài)一:未完成狀態(tài)(unCompleted)
- 執(zhí)行Future內(nèi)部的操作時(在上面的案例中就是具體的網(wǎng)絡(luò)請求過程器贩、我們使用了延遲來模擬)测暗,我們稱這個過程為未完成狀態(tài)。
狀態(tài)二:完成狀態(tài)(completed)
- 當(dāng)Future內(nèi)部的操作執(zhí)行完成磨澡、通常會返回一個值碗啄、或者拋出一個異常
- 這兩種情況、我們都稱為Future為完成狀態(tài)
Dart官網(wǎng)有這兩種狀態(tài)解析稳摄、之所以貼出來時區(qū)別于Promise的三種狀態(tài)補充三:Future的鏈?zhǔn)秸{(diào)用
上面代碼我們可以進行如下改進
- 我們可以在then中繼續(xù)返回值稚字、會在下一個鏈?zhǔn)降膖hen調(diào)用回調(diào)函數(shù)中拿到返回的結(jié)果。
import 'dart:io';
Future<String> getNetWorkData() {
return Future<String>(() {
sleep(Duration(seconds: 3));
/// 返回成功結(jié)果
return 'newWorkData';
/// 返回異常
//throw Exception('網(wǎng)絡(luò)請求出錯');
});
}
main(List<String> args) {
// 打印結(jié)果:
/*
main function start
main function end
value1=newWorkData
value2=content data2
value3=content-data3
*/
print('main function start');
getNetWorkData().then((value1) {
print('value1=$value1');
return 'content data2';
}).then((value2) {
print('value2=$value2');
return 'content-data3';
}).then((value3) {
print('value3=$value3');
});
print('main function end');
}
補充4: Future其他API
4.1: Future.value(value)
- 直接獲取一個完整的Future厦酬、該Future會直接調(diào)用then的回調(diào)函數(shù)胆描。
import 'dart:io';
main(List<String> args) {
/**
main function start
main function end
value=我是Future對象獲取者
*/
print('main function start');
Future.value('我是Future對象獲取者').then((value) {
print('value=$value');
});
print('main function end');
}
為什么會立即執(zhí)行、但是value=我是Future對象獲取者是最后打印的呢仗阅?
- 這是因為Future中的then會作為新的任務(wù)添加到事件隊列中(event Queue)昌讲、加入之后你肯定需要排隊執(zhí)行了。
4.2: Future.error(object)
- 直接獲取一個完成的Future减噪、但是是一個發(fā)生異常的Future短绸、該Future會直接調(diào)用catchError的回調(diào)函數(shù)。
/* 打印結(jié)果:
main function start
main function end
error=Exception: 錯誤信息
*/
print('main function start');
Future.error(Exception('錯誤信息')).catchError((error) {
print('error=${error}');
});
print('main function end');
4.3: Future.delayed(時間筹裕、回調(diào)函數(shù))
- 在延遲一定時間時執(zhí)行回調(diào)函數(shù)醋闭、執(zhí)行完回調(diào)函數(shù)會執(zhí)行then的回調(diào)。
- 之前的案例朝卒、我們也可以使用它來模擬证逻、但是直接學(xué)習(xí)這個API會有點疑惑:
/*
main function start
main function end
value=3秒后的信息
*/
print('main function start');
Future.delayed(Duration(seconds: 3), () {
return '3秒后的信息';
}).then((value) {
print('value=$value');
});
print('main function end');
2.2: await、async
2.2.1: 理論概念理解
如果你已經(jīng)完全搞懂了Future抗斤、那么學(xué)習(xí)await囚企、async應(yīng)該沒啥難度。
await瑞眼、async是什么呢龙宏?
- 它們是Dart中的關(guān)鍵字。
- 它們可以讓我們用同步的代碼去實現(xiàn)異步的操作负拟。
- 并且烦衣、通常一個async的函數(shù)會返回一個Future歹河、
我們已經(jīng)知道掩浙、Future可以做到不阻塞我們的線程、讓線程繼續(xù)執(zhí)行秸歧、并且在完成某個操作時改變自己的狀態(tài)厨姚、并且回調(diào)then或者catchError回調(diào)。如何生成一個Future呢键菱?
- 1: 通過我們前面學(xué)習(xí)的Future構(gòu)造函數(shù)谬墙、或者后面學(xué)習(xí)的Future其他API都可以今布。
- 還有一種就是通過async的函數(shù)。
2.2.2: 案例代碼
- 因為Future.delayed返回的是一個Future對象拭抬、我們不能把它看作成同步的返回結(jié)果: ’netWork data‘去使用
- 也就是我們不能把這個異步的代碼相當(dāng)于同步一樣去使用
main(List<String> args) {
print('main function start');
print(getNetWorkData);
print('main function end');
}
String getNetWorkData() {
var result = Future.delayed(Duration(seconds: 3), () {
return 'netWork data';
});
return '請求的數(shù)據(jù): ' + result;
}
>>>顯示報錯:result 是一個Future對象部默、沒辦法跟String拼接。
現(xiàn)在我們使用await修改下面的代碼:
- 我在Future.delayed函數(shù)前加了一個await
- 一旦有了這個關(guān)鍵字造虎、那么這個操作就會等待Future.delayed執(zhí)行完畢傅蹂、并且等待它的結(jié)果。
main(List<String> args) {
print('main function start');
print(getNetWorkData());
print('main function end');
}
String getNetWorkData() {
var result = await Future.delayed(Duration(seconds: 3), () {
return 'network data';
});
return '請求的數(shù)據(jù)' + result;
}
報錯信息如下:await 關(guān)鍵字只能使用在async函數(shù)中
修改后你會發(fā)現(xiàn)報錯:
- await關(guān)鍵字只能使用在async函數(shù)中算凿。
- 所以我們需要將getNetWorkData函數(shù)定義成async函數(shù)份蝴。
接續(xù)修改上述代碼:
- 在方法后面加入一個關(guān)鍵字 async
- 同時將返回對象寫成如Future<String>
main(List<String> args) {
print('main function start');
print(getNetWorkData());
getNetWorkData().then((value) {
print('value=$value');
});
print('main function end');
}
/// Error: Functions marked 'async' must have a return type assignable to 'Future'
Future<String> getNetWorkData() async {
var result = await Future.delayed(Duration(seconds: 3), () {
return 'network data';
});
return '請求的數(shù)據(jù)' + result;
}
打印結(jié)果:
main function start
Instance of 'Future<String>'
main function end
value=請求的數(shù)據(jù)network data
上述代碼就是我們理想中執(zhí)行的代碼了
- 我們現(xiàn)在可以像同步代碼一樣去使用Future異步返回的結(jié)果了。
- 等待拿到結(jié)果之后和其他數(shù)據(jù)進行拼接氓轰、然后一起返回婚夫。
- 返回的時候并不需要包裝一個Future、直接返回即可署鸡、但是返回的時候返回值會默認(rèn)包裝在一個Future中案糙。
2.3: 讀取json案例
后續(xù)結(jié)合Flutter講解.........
3: Dart的異步補充
3.1: 任務(wù)執(zhí)行順序
3.1.1: 認(rèn)識微任務(wù)隊列
在前面練習(xí)中、我們知道Dart中有一個事件循環(huán)(Event Loop)來執(zhí)行我們的代碼靴庆、里面存在一個事件隊列(Event Queue)侍筛、事件循環(huán)不斷從事件隊列中取出事件執(zhí)行。
但是如果嚴(yán)格來劃分的話撒穷、在Dart中還存在另一個隊列:微任務(wù)隊列(Microtask Queue)匣椰。
- 微任務(wù)隊列的優(yōu)先級要高于事件隊列;
- 也就是說端礼、事件循環(huán)都是優(yōu)先執(zhí)行微任務(wù)隊列中的任務(wù)禽笑、在執(zhí)行事件隊列中的任務(wù)。
那么在Flutter開發(fā)中蛤奥、哪些是放在事件隊列中佳镜、哪些是放在微任務(wù)隊列中呢。- 所有的外部事件任務(wù)都是在事件隊列中凡桥、如IO蟀伸、計時器、點擊缅刽、以及繪制事件等啊掏。
- 而微任務(wù)通常來源于Dart內(nèi)部、并且微任務(wù)非常少衰猛、這是因為如果微任務(wù)非常多迟蜜、那會造成事件隊列排不上隊、會阻塞任務(wù)隊列的執(zhí)行(比如用戶點擊沒有反應(yīng)的情況)啡省。
說到這里娜睛、你可能凌亂了髓霞、在Dart中單線程中、代碼到底是怎樣執(zhí)行的呢畦戒?- 1: Dart的入口是main函數(shù)方库、所以main函數(shù)中的代碼會優(yōu)先執(zhí)行。
- 2: main函數(shù)執(zhí)行完畢后障斋、會啟動一個事件循環(huán)(Event Loop)就會啟動薪捍、啟動后開始執(zhí)行隊列中的任務(wù);
- 3: 首先配喳、會按照先進先出的順序酪穿、執(zhí)行微任務(wù)隊列(Microtask Queue)中的所有任務(wù);
4: 其次晴裹、會按照先進先出的順序被济、執(zhí)行事件隊列(Event Queue)中的所有任務(wù);
3.1.2: 如何創(chuàng)建微任務(wù)
在開發(fā)中涧团、我們可以通過Dart中async下的scheduleMicrotask來創(chuàng)建一個微任務(wù)
// 通過await來修改上述代碼
main(List<String> args) {
print('main function start');
print(getNetWorkData());
getNetWorkData().then((value) {
print('value=$value');
});
print('main function end');
scheduleMicrotask(() {
print('我是scheduleMicrotask創(chuàng)建的微任務(wù)');
});
打印結(jié)果:
main function start
Instance of 'Future<String>'
main function end
我是scheduleMicrotask創(chuàng)建的微任務(wù)
value=請求的數(shù)據(jù)network data
}
在開發(fā)中只磷、如果我們有一個任務(wù)不希望它放在EventQueue中依此排隊、那么就可以創(chuàng)建一個微任務(wù)泌绣。
Future 的代碼是加入到事件隊列只是微任務(wù)隊列呢钮追?
Future中通常有兩個函數(shù)執(zhí)行體:
- Future 構(gòu)造函數(shù)傳入的函數(shù)體
- then的函數(shù)體(catchError等同看待)
那么它們是加入到什么隊列中的呢?
- Future 構(gòu)造函數(shù)傳入的函數(shù)體放在事件隊列中
- then的函數(shù)體要分成三種情況:
- 情況下1: Future 沒有執(zhí)行完成(有任務(wù)需要執(zhí)行)阿迈,那么then會直接被添加到Future的函數(shù)執(zhí)行體后元媚;
- 情況2: 如果Future執(zhí)行完成后就then、該then的函數(shù)體被放到微任務(wù)隊列中苗沧。當(dāng)前Future執(zhí)行完成后執(zhí)行微任務(wù)隊列刊棕;
- 情況3: 如果Future是鏈?zhǔn)秸{(diào)用、意味著then未執(zhí)行完待逞、下一個then不會執(zhí)行甥角;
情況1的代碼案例:
// 打印:
// future_1
// then_1
// future_1加入到eventqueue中识樱、緊隨其后then_1加入到eventqueue中
Future(() => print('future_1')).then((_) => print('then_1'));
情況2的代碼案例:
// 打印then_2
// Future沒有函數(shù)執(zhí)行體(相當(dāng)于Future執(zhí)行完就then)嗤无、then_2被加入到微任務(wù)microtaskqueue中
Future(() => null).then((_) => print('then_2'));
情況3的代碼案例:
/* 打印
future_3
then_3
ehtn_4
*/
// future_3 、then_3怜庸、then_4依此被加入到eventqueue中
Future(() => print('future_3')).then((_) => print('then_3')).then((_) => print('ehtn_4'));
3.1.3: 代碼執(zhí)行順序
我們根據(jù)前面的解讀來測試一下終極的代碼執(zhí)行順序案例:
main(List<String> args) {
print('main function start');
//
Future(() => print('task1'));
//
//
Future(() => print('task2')).then((_) {
print('task3');
scheduleMicrotask(() => print('task4'));
}).then((_) => print('task5'));
//
final future = Future(() => null);
future.then((_) => print('task6'));
//
scheduleMicrotask(() => print('task7'));
//
Future(() => print('task8'))
.then((_) => Future(() => print('task9')))
.then((_) => print('task10'));
//
print('main function end')
;}
/* 打印結(jié)果
main function start
main function end
task7
task1
task6
task2
task3
task5
task4
task8
task9
task10
*/
3.2: 多核CPU的利用
......