概述
- Dart的異步模型
- Dart的異步操作(Future以及async述吸、await)
- Dart的異步補充
一盏求、Dart的異步模型
-
1.1噪漾、Dart是單線程的
-
1.1.1、程序中的耗時操作
開發(fā)中的耗時操作:- 在開發(fā)中蜂莉,我們經(jīng)常會遇到一些耗時的操作需要完成蜡娶,比如網(wǎng)絡(luò)請求、文件讀取等等映穗;
- 如果我們的主線程一直在等待這些耗時的操作完成窖张,那么就會進行阻塞,無法響應(yīng)其它事件男公,比如用戶的點擊荤堪;顯然,我們不能這么干J嗯狻澄阳!
如何處理耗時的操作呢?針對如何處理耗時的操作踏拜,不同的語言有不同的處理方式碎赢。
-
處理方式一
: 多線程,比如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é)果返回即可殉簸。
-
-
1.2、Dart 事件循環(huán)
-
1.2.1沽讹、什么是事件循環(huán)?
單線程模型中主要就是在維護著一個事件循環(huán)(Event Loop)般卑。
事件循環(huán)是什么呢?- 事實上事件循環(huán)并不復(fù)雜爽雄,它就是將需要處理的一系列事件(包括點擊事件蝠检、IO事件、網(wǎng)絡(luò)事件)放在一個事件隊列(Event Queue)中挚瘟。
- 不斷的從事件隊列(Event Queue)中取出事件叹谁,并執(zhí)行其對應(yīng)需要執(zhí)行的代碼塊饲梭,直到事件隊列清空位置。
我們來寫一個事件循環(huán)的偽代碼:
// 這里我使用數(shù)組模擬隊列, 先進先出的原則 List eventQueue = []; var event; // 事件循環(huán)從啟動的一刻焰檩,永遠在執(zhí)行 while (true) { if (eventQueue.length > 0) { // 取出一個事件 event = eventQueue.removeAt(0); // 執(zhí)行該事件 event();000000000000000000000000000000000 } }
當(dāng)我們有一些事件時憔涉,比如點擊事件、IO事件析苫、網(wǎng)絡(luò)事件時监氢,它們就會被加入到eventLoop中,當(dāng)發(fā)現(xiàn)事件隊列不為空時發(fā)現(xiàn)藤违,就會取出事件浪腐,并且執(zhí)行。
事件循環(huán)
齒輪就是我們的事件循環(huán)顿乒,它會從隊列中一次取出事件來執(zhí)行议街。
-
1.2.2、事件循環(huán)代碼模擬
這里我們來看一段偽代碼璧榄,理解點擊事件和網(wǎng)絡(luò)請求的事件是如何被執(zhí)行的:
這是一段Flutter代碼特漩,很多東西大家可能不是特別理解,但是耐心閱讀你會讀懂我們在做什么骨杂。
一個按鈕 RaisedButton涂身,當(dāng)發(fā)生點擊時執(zhí)行 onPressed 函數(shù)。
onPressed 函數(shù)中搓蚪,我們發(fā)送了一個網(wǎng)絡(luò)請求蛤售,請求成功后會執(zhí)行then中的回調(diào)函數(shù)。RaisedButton( child: Text('Click me'), onPressed: () { final myFuture = http.get('https://example.com'); myFuture.then((response) { if (response.statusCode == 200) { print('Success!'); } }); }, )
這些代碼是如何放在事件循環(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í)行献烦,快點幫我完成滓窍。【
-
二巩那、Dart的異步操作
Dart中的異步操作主要使用Future以及async吏夯、await。
如果你之前有過前端的ES6即横、ES7編程經(jīng)驗噪生,那么完全可以將Future理解成Promise,async东囚、await和ES7中基本一致跺嗽。
但是如果沒有前端開發(fā)經(jīng)驗,F(xiàn)uture以及async页藻、await如何理解呢桨嫁?
-
2.1、認(rèn)識 Future
-
2.1.1份帐、同步 的網(wǎng)絡(luò)請求
我們先來看一個例子吧:在這個例子中璃吧,我使用getNetworkData來模擬了一個網(wǎng)絡(luò)請求;該網(wǎng)絡(luò)請求需要3秒鐘的時間废境,之后返回數(shù)據(jù)畜挨;main(List<String> args) { print('開始請求'); String result = getNetworkData(); print('$result'); print('結(jié)束請求'); } String getNetworkData() { sleep(Duration(seconds: 3)); return "我是請求到的數(shù)據(jù)"; }
這段代碼會運行怎么的結(jié)果:getNetworkData會阻塞main函數(shù)的執(zhí)行
開始請求 // 等待 3 秒 我是請求到的數(shù)據(jù) 結(jié)束請求
顯然,上面的代碼不是我們想要的執(zhí)行效果彬坏,因為網(wǎng)絡(luò)請求阻塞了main 函數(shù)朦促,那么意味著其后所有的代碼都無法正常的繼續(xù)執(zhí)行。
-
2.1.2栓始、異步的網(wǎng)絡(luò)請求
我們來對我們上面的代碼進行改進,和剛才的代碼唯一的區(qū)別在于我使用了Future對象來將耗時的操作放在了其中傳入的函數(shù)中血当;代碼如下:import 'dart:io'; main(List<String> args) { print('開始請求'); var future = getNetworkData(); future.then((String value) { print("結(jié)果:$value"); }).catchError((error) { print('$error'); }).whenComplete(() { print('代碼執(zhí)行完成'); }); print('結(jié)束請求'); } // 模擬一個網(wǎng)絡(luò)請求 Future<String> getNetworkData() { return Future<String>(() { // 將耗時操作包裹到 Future的函數(shù)回調(diào)中 // 1>幻赚、只要有返回結(jié)果,那么就執(zhí)行 Future對應(yīng)的回調(diào)(Promise-resolve) // 2>臊旭、如果沒有結(jié)果返回(有錯誤信息)落恼,需要在 Future 會調(diào)中跑出一個異常(Promise-reject) sleep(Duration(seconds: 3)); throw Exception("我是錯誤信息"); // return "我是請求到的數(shù)據(jù)"; }); }
提示:
- 1、請求錯誤拋出異常:
throw Exception("我是錯誤信息");
- 2离熏、代碼執(zhí)行完:whenComplete
- 1、請求錯誤拋出異常:
-
-
2.1.3. Future 使用補充
-
Future 的鏈?zhǔn)秸{(diào)用:可以在 then 中繼續(xù)返回值佳谦,會在下一個鏈?zhǔn)降膖hen調(diào)用回調(diào)函數(shù)中拿到返回的結(jié)果,實例代碼如下
import 'dart:io'; main(List<String> args) { print('start'); Future(() { sleep(Duration(seconds: 3)); throw Exception("第一次異常"); // return '第1次網(wǎng)絡(luò)請求的結(jié)果'; }).then((result) { print('$result'); sleep(Duration(seconds: 3)); return '第2次網(wǎng)絡(luò)請求的結(jié)果'; }).then((result) { print('$result'); sleep(Duration(seconds: 3)); return '第3次網(wǎng)絡(luò)請求的結(jié)果'; }).then((result) { print('$result'); }).catchError((error){ print(error); }); print('end'); }
打印結(jié)果如下:
start end 第1次的結(jié)果 第2次的結(jié)果 第3次的結(jié)果
-
Future 其他API
-
Future.value(value)
滋戳,直接獲取一個完成的Future钻蔑,該Future會直接調(diào)用then的回調(diào)函數(shù)main(List<String> args) { print('Main Start'); Future.value('內(nèi)容').then((value) { print(value); }); print('Main End'); }
打印結(jié)果如下:
Main Start Main End 內(nèi)容
疑惑:為什么立即執(zhí)行啥刻,但是哈哈哈是在最后打印的呢?
這是因為Future中的then會作為新的任務(wù)會加入到事件隊列中(Event Queue)咪笑,加入之后你肯定需要排隊執(zhí)行了 -
Future.error(object)
:直接獲取一個完成的Future可帽,但是是一個發(fā)生異常的Future,該Future會直接調(diào)用catchError的回調(diào)函數(shù)main(List<String> args) { print("main function start"); Future.error(Exception("錯誤信息")).catchError((error) { print(error); }); print("main function end"); }
打印結(jié)果如下:
main function start main function end Exception: 錯誤信息
-
Future.delayed
(時間, 回調(diào)函數(shù)):在延遲一定時間時執(zhí)行回調(diào)函數(shù)窗怒,執(zhí)行完回調(diào)函數(shù)后會執(zhí)行 then 的回調(diào)映跟;main(List<String> args) { print("main function start"); Future.delayed(Duration(seconds: 3), () { return "3秒后的信息"; }).then((value) { print(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)鍵字(你這不是廢話嗎贷洲?廢話也還是要強調(diào)的,萬一你用它做變量名呢晋柱。)
- 它們可以讓我們用 同步的代碼格式优构,去實現(xiàn)異步的調(diào)用過程。
- 并且雁竞,通常一個async的函數(shù)會返回一個Future(別著急钦椭,馬上就看到代碼了)。
我們已經(jīng)知道碑诉,F(xiàn)uture可以做到不阻塞我們的線程彪腔,讓線程繼續(xù)執(zhí)行,并且在完成某個操作時改變自己的狀態(tài)进栽,并且回調(diào)then或者errorCatch回調(diào)德挣。
如何生成一個Future呢?- 1快毛、通過我們前面學(xué)習(xí)的Future構(gòu)造函數(shù)格嗅,或者后面學(xué)習(xí)的Future其他API都可以。
- 2唠帝、還有一種就是通過async的函數(shù)屯掖。
-
2.2.2、案例代碼演練
我們來對之前的Future異步處理代碼進行改造襟衰,改成await贴铜、async的形式。
我們知道,如果直接這樣寫代碼绍坝,代碼是不能正常執(zhí)行的:因為 Future.delayed 返回的是一個Future對象徘意,我們不能把它看成同步的返回數(shù)據(jù):"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"; }).then((value) { print('結(jié)果是:$value'); }); print("哈哈"); return "請求到的數(shù)據(jù):$result"; }
打印結(jié)果如下
main function start 哈哈 請求到的數(shù)據(jù):Instance of 'Future<String>' main function end 結(jié)果是:network data
現(xiàn)在我使用
await
修改下面這句代碼:1陷嘴、你會發(fā)現(xiàn)映砖,我在Future.delayed函數(shù)前加了一個await。2灾挨、一旦有了這個關(guān)鍵字邑退,那么這個操作就會等待Future.delayed的執(zhí)行完畢,并且等待它的結(jié)果劳澄。Future<String> getNetworkData() async { var result = await Future.delayed(Duration(seconds: 3), () { return "network data"; }).then((value) { print('結(jié)果是:$value'); }); print("哈哈"); return "請求到的數(shù)據(jù):$result"; }
提示:
- 1地技、await關(guān)鍵字必須存在于async函數(shù)中。
- 2秒拔、使用async標(biāo)記的函數(shù)莫矗,必須返回一個Future對象。
- 3砂缩、我們現(xiàn)在可以像同步代碼一樣去使用Future異步返回的結(jié)果作谚;等待拿到結(jié)果之后和其他數(shù)據(jù)進行拼接,然后一起返回庵芭;返回的時候并不需要包裝一個Future妹懒,直接返回即可,但是返回值會默認(rèn)被包裝在一個Future中双吆;
-
三眨唬、Dart的異步補充
-
3.1、任務(wù)執(zhí)行順序
-
3.1.1好乐、認(rèn)識微任務(wù)隊列
在前面學(xué)習(xí)學(xué)習(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)的情況)艰垂;
說道這里,你可能已經(jī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ù):import "dart:async"; main(List<String> args) { scheduleMicrotask(() { print("Hello Microtask"); }); }
在開發(fā)中,如果我們有一個任務(wù)不希望它放在Event Queue中依次排隊蒋院,那么就可以創(chuàng)建一個微任務(wù)了亏钩。
Future的代碼是加入到事件隊列還是微任務(wù)隊列呢?
Future中通常有兩個函數(shù)執(zhí)行體:- Future構(gòu)造函數(shù)傳入的函數(shù)體
- then的函數(shù)體(catchError等同看待)
那么它們是加入到什么隊列中的呢欺旧?
Future構(gòu)造函數(shù)傳入的函數(shù)體放在事件隊列中
then的函數(shù)體要分成三種情況:
情況一:Future沒有執(zhí)行完成(有任務(wù)需要執(zhí)行)姑丑,那么then會直接被添加到Future的函數(shù)執(zhí)行體后;
情況二:如果Future執(zhí)行完后就then辞友,該then的函數(shù)體被放到如微任務(wù)隊列栅哀,當(dāng)前Future執(zhí)行完后執(zhí)行微任務(wù)隊列震肮;
-
情況三:如果Future是鏈?zhǔn)秸{(diào)用,意味著then未執(zhí)行完留拾,下一個then不會執(zhí)行戳晌;
// future_1加入到eventqueue中,緊隨其后then_1被加入到eventqueue中 Future(() => print("future_1")).then((_) => print("then_1")); // Future沒有函數(shù)執(zhí)行體痴柔,then_2被加入到microtaskqueue中 Future(() => null).then((_) => print("then_2")); // future_3沦偎、then_3_a、then_3_b依次加入到eventqueue中 Future(() => print("future_3")).then((_) => print("then_3_a")).then((_) => print("then_3_b"));
-
3.1.3. 代碼執(zhí)行順序
import "dart:async"; main(List<String> args) { print("main start"); Future(() => print("task1")); final future = Future(() => null); Future(() => print("task2")).then((_) { print("task3"); scheduleMicrotask(() => print('task4')); }).then((_) => print("task5")); future.then((_) => print("task6")); scheduleMicrotask(() => print('task7')); Future(() => print('task8')).then((_) => Future(() => print('task9'))).then((_) => print('task10')); print("main end"); }
代碼執(zhí)行的結(jié)果是:
main start main end task7 task1 task6 task2 task3 task5 task4 task8 task9 task10
代碼分析:
- 1咳蔚、main函數(shù)先執(zhí)行豪嚎,所以main start和main end先執(zhí)行,沒有任何問題屹篓;
- 2疙渣、main函數(shù)執(zhí)行過程中,會將一些任務(wù)分別加入到EventQueue和MicrotaskQueue中堆巧;
- 3妄荔、task7通過scheduleMicrotask函數(shù)調(diào)用,所以它被最早加入到MicrotaskQueue谍肤,會被先執(zhí)行啦租;
- 4、然后開始執(zhí)行EventQueue荒揣,task1被添加到EventQueue中被執(zhí)行篷角;
- 5、通過final future = Future(() => null);創(chuàng)建的future的then被添加到微任務(wù)中,微任務(wù)直接被優(yōu)先執(zhí)行,所以會執(zhí)行task6滑沧;
- 6、一次在EventQueue中添加task2嘉蕾、task3、task5被執(zhí)行霜旧;
- 7错忱、task3的打印執(zhí)行完后,調(diào)用scheduleMicrotask挂据,那么在執(zhí)行完這次的EventQueue后會執(zhí)行以清,所以在task5后執(zhí)行task4(注意:scheduleMicrotask的調(diào)用是作為task3的一部分代碼,所以task4是要在task5之后執(zhí)行的)
- 8崎逃、task8掷倔、task9、task10一次添加到EventQueue被執(zhí)行个绍;
事實上今魔,上面的代碼執(zhí)行順序有可能出現(xiàn)在面試中勺像,我們開發(fā)中通常不會出現(xiàn)這種復(fù)雜的嵌套障贸,并且需要完全搞清楚它的執(zhí)行順序错森;
但是,了解上面的代碼執(zhí)行順序篮洁,會讓你對EventQueue
和microtaskQueue
有更加深刻的理解涩维。
-
-
3.2. 多核CPU的利用
-
3.2.1.
Isolate
的理解
在Dart中,有一個Isolate的概念袁波,它是什么呢瓦阐?- 我們已經(jīng)知道Dart是單線程的,這個線程有自己可以訪問的內(nèi)存空間以及需要運行的事件循環(huán)篷牌;
- 我們可以將這個空間系統(tǒng)稱之為是一個Isolate睡蟋;
- 比如Flutter中就有一個Root Isolate,負責(zé)運行Flutter的代碼枷颊,比如UI渲染戳杀、用戶交互等等;
在 Isolate 中夭苗,資源隔離做得非常好信卡,每個 Isolate 都有自己的 Event Loop 與 Queue,
- Isolate 之間不共享任何資源题造,只能依靠消息機制通信傍菇,因此也就沒有資源搶占問題。
但是界赔,如果只有一個Isolate丢习,那么意味著我們只能永遠利用一個線程,這對于多核CPU來說淮悼,是一種資源的浪費咐低。
如果在開發(fā)中,我們有非常多耗時的計算敛惊,完全可以自己創(chuàng)建Isolate渊鞋,在獨立的Isolate中完成想要的計算操作。
如何創(chuàng)建Isolate呢瞧挤?我們通過Isolate.spawn就可以創(chuàng)建了:如下import "dart:isolate"; main(List<String> args) { Isolate.spawn(foo, "Hello Isolate"); } void foo(info) { print("新的isolate:$info"); }
提示:
Isolate.spawn(foo, "Hello Isolate");
第一個參數(shù)是函數(shù)名锡宋,第二個是參數(shù) 3.2.2. Isolate通信機制
但是在真實開發(fā)中,我們不會只是簡單的開啟一個新的Isolate特恬,而不關(guān)心它的運行結(jié)果:我們需要新的Isolate進行計算执俩,并且將計算結(jié)果告知Main Isolate(也就是默認(rèn)開啟的Isolate);
Isolate 通過發(fā)送管道(SendPort)實現(xiàn)消息通信機制癌刽;
我們可以在啟動并發(fā)Isolate時將Main Isolate的發(fā)送管道作為參數(shù)傳遞給它役首;
-
并發(fā)在執(zhí)行完畢時尝丐,可以利用這個管道給Main Isolate發(fā)送消息;
import "dart:isolate"; main(List<String> args) async { // 1.創(chuàng)建管道 ReceivePort receivePort= ReceivePort(); // 2.創(chuàng)建新的Isolate Isolate isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort); // 3.監(jiān)聽管道消息 receivePort.listen((data) { print('Data:$data'); // 不再使用時衡奥,我們會關(guān)閉管道 receivePort.close(); // 需要將isolate殺死 isolate?.kill(priority: Isolate.immediate); }); } void foo(SendPort sendPort) { sendPort.send("Hello World"); }
但是我們上面的通信變成了單向通信爹袁,如果需要雙向通信呢?
- 事實上雙向通信的代碼會比較麻煩矮固;
- Flutter提供了支持并發(fā)計算的
compute
函數(shù)失息,它內(nèi)部封裝了Isolate
的創(chuàng)建和雙向通信; - 利用它我們可以充分利用多核心CPU档址,并且使用起來也非常簡單盹兢;
注意:下面的代碼不是dart的API,而是Flutter的API守伸,所以只有在Flutter項目中才能運行
main(List<String> args) async { int result = await compute(powerNum, 5); print(result); } int powerNum(int num) { return num * num; }
提示:
compute(powerNum, 5);
傳入的函數(shù)
必須是全局的方法
-