(五)flutter入門之dart中的并發編程、異步和事件驅動詳解

上篇博客介紹了dart提供的非常靈活的類操作,接下來介紹dart中的并發編程、異步操作以及dart中的事件驅動

并發編程

我們知道dart是個單線程的語言,和js一樣,所以dart中不存在多線程操作,那么我們如果遇到多任務并行的場景,該如何去做呢?dart中提供了一個類似于java新線程但不能共享內存的獨立運行的worker ,屬于一個新的獨立的Dart執行環境--isolate,像我們執行任務的時候默認的main方法就是一個默認的isolate,可以看出如果我們想在dart中執行多個并行的任務,可以選擇創建多個isolate來完成,那么isolate之間如何交互?isolate自身的任務又是如何處理的?

Isolate.spawn

在dart中,一個Isolate對象其實就是一個isolate執行環境的引用,一般來說我們都是通過當前的isolate去控制其他的isolate完成彼此之間的交互,而當我們想要創建一個新的Isolate可以使用Isolate.spawn方法獲取返回的一個新的isolate對象,我們來看下spawn方法的源碼

spawn<T>( 
    void entryPoint(T message), 
    T message, { 
        bool paused: false, 
        bool errorsAreFatal, 
        SendPort onExit, 
        SendPort onError 
    } 
) → Future<Isolate>//可以看出來最終會返回一個Isolate對象,至于Future是什么,接下來會介紹

一般來說我們使用spawn創建的isolate自身帶有控制接口(control port )和可控制對象的能力(capability ),當然我們也可以不擁有這個能力,那么isolate之間是如何進行交互操作的?我們看下流程圖

isolate交互.png

從上圖中我們可以看到兩個isolate之間使用SendPort相互發送消息,而isolate中也存在了一個與之對應的ReceivePort接受消息用來處理,但是我們需要注意的是,ReceivePort和SendPort在每個isolate都有一對,只有同一個isolate中的ReceivePort才能接受到當前類的SendPort發送的消息并且處理(可以看出來谷歌這么設計的意義就是防止多個isolate之間接受器混亂),而isolate的spawn就是用來創建帶有控制能力的isolate,第二個參數就可以選擇傳遞當前Isolate的SendPort,交給新創建的實例,這樣新創建的實例就可以發送消息給原來的isolate,實現兩者之間通訊,接下來我們看一下Isolate交互的實例:

import 'dart:isolate';
int i;

void main() {
  i = 10;
  SendPort childSendPort;
  //創建一個消息接收器--這里創建的是默認的main的isolate的,我們可以稱之為主進程
  ReceivePort receivePort = new ReceivePort();
  //創建新的具有發送器的isolate,第一個參數是具有內存隔離的新的isolate的具體業務邏輯函數,第二個是創建的isolate的時候傳遞的參數,一般我們傳遞當前isolate的發送器
  Isolate.spawn(isolateMain, receivePort.sendPort);

  //主進程接受持有主進程發送器的isolate發過來的消息
  receivePort.listen((message) {
    //其他的isolate可以選擇發過來自身的sendPort給主進程,則主進程isolate也可以向創建的isolate發送消息,完成交互操作
    if (message is SendPort) {
      message.send("已收到子Isolate的發送器!!");
      childSendPort =message;
    } else {
      print("接到子isolate消息:" + message);
      //進行一次回復
      if(childSendPort != null){
        childSendPort.send('已收到你的消息');
      }
    }
  });
}

/// 內存隔離的新的isolate的具體業務邏輯函數
void isolateMain(SendPort sendPort) {
  // isolate是內存隔離的,i的值是在其他isolate定義的(默認都是主isolate環境)所以這里獲得null
  print(i);//輸出:--->null

  //當前isolate的消息接收器
  ReceivePort receivePort = new ReceivePort();
  //創建當前子isolate的時候傳遞的第二個參數(這里我們認為是該iso的發送器),使用主iso的發送器將自身子iso的發送器發送過去,完成交互
  sendPort.send(receivePort.sendPort);


  // 測試向主isolate發送消息
  sendPort.send("你收到我的消息了嗎?");
  receivePort.listen((message) {
    print("接到主isolate消息:" + message);
  });
}

事件驅動

我們在上面有提到,每一個isolate相當于一個完全獨立的dart執行環境,那么當前的環境中如果存在一些任務,如果完全按照順序執行,豈不是會因為某個任務處理的時間過于久,后面的任務來不及執行?在單線程語言中,如果不做任何處理,的確會出現這種問題,熟悉js的人都知道js的異步操作很優秀,原因在于js有著不錯的事件驅動進行任務調度,提高任務執行的效率,尤其是最近熱門的node.js,更是把事件驅動做到極致,而dart作為一個優秀的單線程語言,自然不可能缺少事件驅動這個優秀的特性,在dart中事件驅動是存在于isolate中的,也就是說,我們每一個新的isolate都有一個獨立的完整的event-loop,而每一個Loop中又包含了兩個隊列,其中一個隊列叫microtask queue(微任務隊列),該隊列的執行優先級最高,而另外一個隊列event queue(事件隊列)屬于普通的事件隊列,兩個隊列依靠著固定的執行流程完成了整個的dart任務執行機制,事件驅動的執行流程圖如下:

isolate中的消息機制.png

從上圖中我們可以很清晰的看出來兩個隊列之間的微妙的執行流程:

  • microtask-queue的執行優先于event-queue,并且在microtask-queue中是輪詢執行的,也就是說,microtask-queue中所有的任務執行完成以后才會去event-queue中執行任務
  • event-queue中的任務執行級別最低,每一個任務執行完畢以后,都會重新去輪詢一次microtask-queue,如果這個時候microtask-queue中有了新的任務,那么不用說,肯定會把microtask-queue再次全部執行完再回到event-queue中執行

并且我們平時執行的任務絕大多數都是在event-queue中執行的,所以我們正常的任務如果想要執行,microtask-queue中盡量不要加入太多復雜的業務操作,但同時我們也可以看出來,dart中存在著‘插隊’機制,即我們希望某個任務優先于其他任務先去執行,我們可以選擇將任務丟進microtask-queue優先處理,接下來我們先看一個案例:

import 'dart:io';

void main(){
  new File("C:\Users\Administrator\Desktop\http通用類.txt").readAsString().then((content){
      print(content);//按理來說應該會輸出文件中每一行的數據的,但是一直不輸出
  });
  while(true){}
}

從上面的案例的結果可以看出來,程序一直阻塞著,永遠不執行文件io輸出的內容,這是為什么呢?原因很簡單,因為io流是異步的操作,并且than方法會把任務加入到event-queue,這個時候main函數的while循環早于文件io執行,就會一直阻塞程序,所以在dart中合理分配microtask-queue和event-queue很重要,同樣因為我們這里用了異步的任務,導致了任務隊列執行順序的變化,所以合理運用同步任務和異步任務在dart開發中也格外重要。接下來我們學習dart中的異步任務和執行器Future

Future:

在java開發中,我們進行并發編程的時候,經常會使用future來處理任務,獲取返回的結果,或者延遲處理任務等操作,而在dart中,我們執行一個任務同樣也可以使用future來處理,而Future默認情況下執行的是一個event-queue任務,也就是和我們正常執行的業務邏輯一樣的任務級別,然而future提供了多個api和特性來完成任務隊列的使用,例如我們在js開發的時候經常使用ajax的then函數進行回調,then中的參數即為當前任務執行完的參數,我們可以在then中處理接下來的業務,future也具有同樣的編程特性,then函數和catch函數分別處理任務返回的結果以及出現的異常,同樣的,future作為一個任務操作者,也提供了延遲任務和可以加入microtask-queue的特殊任務(Future.microtask函數),這樣我們就可以通過future完成一系列的任務操作,接下來我們看一個Future操作的案例,從而熟悉Future的常用操作:

import 'dart:async';
void main(){
  Future.delayed(new Duration(seconds:3),(){
    //3s以后執行的普通任務
  }).then((value){
    //then函數中可以獲取最終執行完返回的結果,并且需要注意的是then可以連續回調使用完成一系列操作
  }).catchError((error){
    //catchError函數中會捕捉當前任務執行流程中的所有的異常,包括每一步的then函數中出現的異常,一旦有異常就會停止任務執行,直接進入當前函數
  });
  
  //future內部就是調用了scheduleMicrotask函數,用來將當前任務加入到microtask-queue中,實現'插隊'功能
  Future.microtask((){
    //優先執行的業務邏輯
  });
    Future.sync((){
     //同步運行的任務:同步運行指的是構造Future的時候傳入的函數是同步運行的,當前傳遞的函數可以一起執行,和then不同的是,then回調進來的函數是調度到微任務隊列異步執行的
    });
    //scheduleMicrotask((){
    //   print('a microtask');
    //});
}

從上面的代碼中我們不難發現一個特點,Future既然有豐富的任務相關的處理Api,開發的過程中肯定會比較頻繁的使用,但是我們對Future熟悉以后,就會發現一個特點,Future的很多Api都是類似構造者模式(js中的promise模式),我們可以無限的繼續回調下去,尤其是then函數,我們在開發的過程中可能存在一個比較復雜的業務,如果不能使用promise的方式處理的話會是什么情況?我們來看一個假設的情況:

//我們先定義幾個連續的異步操作任務,首先調用登錄-->獲取用戶信息-->保存信息
Future<String> login(String userName, String pwd){
  //用戶登錄
}

Future<String> getUserInfo(String id){
  //獲取用戶信息
}

Future saveUserInfo(String userInfo){
  // 保存用戶信息
}

接著我們來按照這些方法進行業務開發:

login("admin","123456").then((id){
  getUserInfo(id).then((user){
    saveUserInfo(userInfo).then((){
      //這里再去執行其他任務。。。。
    });
  });
});

是不是發現了問題?一個回調內部調用另外一個,嵌套調用的次數太多了,我們可能陷入了一個恐怖的回調地獄中,那么promise方式下的代碼開發就輕松太多了

void main(){
  Future((){
    //用戶登錄
  }).then((value){
    //獲取用戶信息
  })
  .then((value){
    //保存用戶信息
  })
  .then((value){
    //業務1
  })
  .then((value){
    //業務2
  })
   .........//這里可以無限的回調處理下去
}

是不是整體看起來邏輯更清晰了?但是我們不禁犯難了,因為promise模式的確能改觀一部分問題,但是當我們業務更加復雜的時候,依然會存在一次回調操作,看起來依然很復雜,那么又該如何呢?我們知道js為了改觀promise的這個弊端,在es7中引入了async/await 關鍵字來解決該問題,同樣dart中也引入了async/await可以更加優雅的處理地獄回調

async/await

aync和await很明顯可以看出來,一個是同步操作,一個是同步等待,需要注意的是這兩個關鍵字不是單獨使用的,需要兩個關鍵字一起配合完成整體的代碼同步操作,接下來我們通過案例來看看async/await為何能更優雅的解決地獄回調:

void main() async{
   try{
    //每一個方法前加入await代表當前方法是同步的,執行完以后才會繼續執行后續的操作
    String id = await login("admin","123456");
    String userInfo = await getUserInfo(id);
    await saveUserInfo(userInfo);
   } catch(e){
    //錯誤處理   
    print(e);   
   }  
}

Future<String> login(String userName, String pwd){
//用戶登錄
}

Future<String> getUserInfo(String id){
//獲取用戶信息
}

Future saveUserInfo(String userInfo){
// 保存用戶信息
}

當然,dart中還提供了另外一個場景的實現,比如我們可能需要某幾個任務在一個階段完成以后才可以執行其他的任務,但是我們對于優先執行的幾個任務的執行順序沒有強制要求,但是我們要求必須是這幾個完成以后才能執行其他的任務,這個時候,我們可以選擇按照順序去編寫Future任務,或者指定幾個Future.microtask任務優先執行,但是在future中同樣提供了一個wait操作,可以同時執行多個任務,等待全部完成后才會進行回調操作,案例如下:

void main(){
   try{
    //假設我們現在登錄和獲取用戶信息操作是一組,都執行完畢以后才可以執行保存用戶登錄成功的操作
    Future.wait([loginFuture,getUserInfoFuture]).then((values){
       //這里values是個數組,分別是每一個任務返回的結果,
       print(values[0]);//打印第一個任務的結果
       saveUserInfo('admin');
    });
   } catch(e){
    //錯誤處理   
    print(e);   
   }  
}

Future loginFuture = Future<String>((){
   //這里調用登錄操作
   login('admin','123456');
});
String login(String userName, String pwd){
  //登錄操作
}
bool getUserInfo(int id){
  //獲取用戶信息
}
Future<String> getUserInfoFuture =Future((){
  getUserInfo(1);
});

Future saveUserInfo(String userInfo){
// 保存用戶信息
}

注意:無論是在JavaScript還是Dart中,async/await都只是一個語法糖,編譯器或解釋器最終都會將其轉化為一個Promise(Future)的調用鏈

Stream

Stream操作也是dart中提供的用來處理異步操作的工具,和Future不同的是它可以接收多個異步操作的結果(無論成功或失敗) ,我們可以理解為:執行異步任務時,可以通過多次觸發成功或失敗事件來傳遞結果數據或錯誤異常 ,例如我們開發的過程中經常見到的場景:多個文件讀寫,網絡下載可能會發起多個等,接下來我們根據一個案例分析一下Stream常用的操作:

void main(){
   Stream.fromFutures([loginFuture,getUserInfoFuture,saveUserInfoFuture])
         .listen((data){
          //各個任務返回的結果的回調
         },onError:((err){
           //各個任務執行失敗的回調
         }),
         onDone : ((){
           //監聽各個任務執行的過程中完成的時候的回調
         })
         )
         .onDone((){
           //所有任務全部完成的回調
         });
}

Future loginFuture = Future<String>((){
   //這里調用登錄操作
   login('admin','123456');
});
String login(String userName, String pwd){
  //登錄操作
}
bool getUserInfo(int id){
  //獲取用戶信息
}
Future<String> getUserInfoFuture =Future((){
  getUserInfo(1);
});

Future saveUserInfoFuture = Future((){
   saveUserInfo("admin");
});

void saveUserInfo(String userInfo){

}

可以看到listen方法中我們可以對每一個任務進行精細的回調處理,甚至所有任務執行完畢以后,我們還有cancel |pause | resume |onError |onDone 等回調可以分別對整個組任務執行過程的不同階段進行精細度的處理,在上面我們使用了fromFutures方法使用Stream,除此之外,Stream使用過程中我們也經常用fromFuture 方法用來處理單個Futrue/fromIterable 方法處理集合中的數據。當然,除了這種常規的Stream操作以外,dart還提供了兩個專門操作/創建流的類,可以實現流操作的復雜操作。

StreamController

在dart中為我們設計了一個可以創建新的流用來處理任務的控制器類--StreamController,我們可以在StreamController上創建流,發送數據以及獲取數據進行處理等操作,接下來我們通過案例學習StreamController的常用操作:

//任意類型的流
StreamController controller = StreamController();
//監聽這個流的出口,當有data流出時,打印這個data
StreamSubscription subscription = controller.stream.listen((data){
    print(data);
  });
//sink作為流的入口,使用了sink的add可以往當前流中塞入數據,這個時候數據就可以被監聽到
controller.sink.add(123);
controller.sink.add("abc");

//創建一條處理int類型的流
StreamController<int> numController = StreamController();
numController.sink.add(123);
Transforming an existing stream

在dart中,除了StreamController這種可以創建一個新的流的方式以外,還提供了一個特殊的操作的工具集,可以將當前已有的流轉換為一個新的流--Transforming an existing stream,使用方法就是流自身提供的map(),where(),expand(),和take()方法 ,接下來我們看看這幾個方法有什么用處:

  • where:where的作用就是用來過濾當前流中一些不需要的數據,比如我們當前需要獲取一個int流中的所有的奇數,這個時候就可以使用where進行過濾,做出一定的響應事件,需要注意的是,where方法中會傳遞一個event,無論我們需要還是不需要都會傳遞

  • take:take可以用來限制我們當前輸入到流的次數,也就是觸發流傳輸的事件的次數,比如存在一個場景,用戶輸入密碼三次以后就不可以輸入比較,這個時候就可以使用take限制數量為3,即只能觸發三次

  • transform :transform是用來轉換流控制的方法,我們可以使用該方法和StreamTransformer 配合使用,實現我們自己對流操作的完全控制,比如我們現在有需求對流中的數據進行不同的業務操作,案例如下:

    import 'dart:async';
    
    void main(){
     StreamController<int> controller = StreamController<int>();
    //transformer是對流數據控制的具體實現,比如流中數據如何處理等
    final transformer = StreamTransformer<int,String>.fromHandlers(
        handleData:(value, sink){
        if(value==1){
          //我們限制value==1的時候,觸發正確的操作
          sink.add("操作正確");
        }
        else{ sink.addError('操作失敗,再試一次吧');
        }
      });
      
      //創建流的時候調用transform將當前的流的操作交由控制器處理
      controller.stream
                .transform(transformer)
                .listen(
                    (data){
                      //控制器處理以后還能被sink.add添加進來,說明當前是正確的數據,在當前方法輸出
                      print(data);
                    },
                    onError:(err){
                      //錯誤的操作會在當前方法中輸出:-->操作失敗,再試一次吧
                      print(err);
                    });
        
        controller.sink.add(50);
     }
    
Stream的多訂閱模式

在上面的案例上,我們使用過Stream用來執行一些任務,如果想要監控Stream,可以使用listen來返回一個訂閱者StreamSubscription,當然還可以使用onData方法重置數據(監聽也會重置),但是我們需要注意的是Stream默認的情況下只能是單監聽,例如:

 import 'dart:async';
 import 'dart:io';

void main(){
  Stream<List<int>> stream = new File("C:/Users/Administrator/http通用類.txt").openRead();
  StreamSubscription<List<int>> listen =  stream.listen((List<int> bytes) {
    print('開啟第一個監聽');
  });
  //默認單訂閱模式下不可以開啟多個listener,即使是第一個監聽已經關閉停止也不可以開啟第二個監聽,否則會輸出Bad state: Stream has already been listened to.的異常
  //StreamSubscription<List<int>> listen2 =  stream.listen((List<int> bytes) {
  //  print('開啟第二個監聽');
  //});
  listen.onData((_){
    print("重新開啟數據和監聽。。。");
  });
  listen.onDone((){
    //監聽完成以后的操作
  });
  listen.onError((e,s){
    //監聽出現異常的操作
  });
  //暫停監聽,如果后面沒有操作繼續監聽,就會停止
  listen.pause();
  //暫停狀態下會恢復監聽
  listen.resume();
 }

但是在開發的過程中,我們有很多場景下可能多個類都需要對這個事件進行監聽操作或者要對這個事件進行不一樣的監聽操作,這個時候我們就該開啟Stream的多訂閱廣播模式,開啟廣播模式,我們需要使用 Stream.asBroadcastStream()方法進行申明一個可以被多個監聽監控的Stream

 import 'dart:async';
 import 'dart:io';

void main(){
   Stream<List<int>> stream = new File("C:/Users/Administrator/http通用類.txt").openRead().asBroadcastStream();
   StreamSubscription<List<int>> listen =  stream.listen((List<int> bytes) {
    print("第一個監聽");
  });
  //asBroadcastStream申明了一個可以多個訂閱者存在的監聽操作,這個時候不會報任何異常,能正常輸出
  StreamSubscription<List<int>> listen2 =  stream.listen((List<int> bytes) {
    print("第二個監聽");
  });
 }

這樣的Stream就可以實現多個監聽了,同樣的,我們不禁有一個思考,Stream實現多訂閱的情況下和我們原生安卓開發的EventBus庫的功能不是很類似嗎?那我們能否實現一個適合Dart的Event-bus呢?接下來我們來實現一個簡易的event-bus:

event_bus.dart文件代碼如下:

import 'dart:async';

class EventBus{
  static EventBus _eventBus;
  StreamController _streamController;
  final Map<String,StreamController> _registerMap = new Map<String,StreamController>();
  final String defName = "default";//默認的廣播

  EventBus._();

  static EventBus getDefault(){
    if(_eventBus == null){
      _eventBus = new EventBus._();
    }
    return _eventBus;
  }
  
  //使用eventBus的時候,需要注冊,可以指定注冊名,可以使用默認注冊
  void register(listener,{String registerName}){
     if(null ==registerName){
      registerName = defName;
     }
     if(!_registerMap.containsKey(registerName)){
       _streamController = StreamController.broadcast();
       _registerMap[registerName] = _streamController;   
     }
     _registerMap[registerName].stream.listen(listener);
  }

  //不使用的時候可以取消注冊
  void unRegister({String registerName}){
    if(null ==registerName){
      registerName =defName;
    }
    _registerMap[registerName].close();
    _registerMap.remove(registerName);
  }

  //針對當前注冊的流進行通訊,如果流不存在就不發消息,防止出現取消注冊以后報錯的情況,當然我們也可以在不存在的情況拋一個異常
  void post(msg,{String registerName}){
    if(null ==registerName){
      registerName =defName;
    }
    if(_registerMap.containsKey(registerName)){
     _registerMap[registerName].add(msg);
    }
  }
}

接下來是main.dart文件的代碼:

import 'event_bus.dart';

void main() {
  EventBus.getDefault().register(onListener);
  //發送消息
  EventBus.getDefault().post('你猜猜我發了什么?');
  //取消注冊
  EventBus.getDefault().unRegister();
}

void onListener(msg){
   print('接受來的消息為:'+msg);//最終輸出為:接受來的消息為:你猜猜我發了什么?
}

好了,至此,flutter入門之dart相關常見的內容我們學習的差不多了,接下來就是開始flutter之旅了(由于flutter禁止了反射,所以針對dart中的反射等操作,這里我就不去進行深入討論,有原生開發java功底的可以自己查資料看下,和java的反射很像)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容

  • 以下內容從官網得到:https://webdev.dartlang.org/articles/performanc...
    None_Ling閱讀 37,059評論 7 34
  • Mixin 備注:子類沒有重寫超類A方法的前提下,如果2個或多個超類擁有相同簽名的A方法,那么子類會以繼承的最后一...
    小流星雨閱讀 1,358評論 0 2
  • 《The Event Loop and Dart》譯文 原文:https://webdev.dartlang.or...
    無玄閱讀 876評論 0 2
  • 前言 我們所熟悉的前端開發框架大都是事件驅動的。事件驅動意味著你的程序中必然存在事件循環和事件隊列。事件循環會不停...
    HowHardCanItBe閱讀 15,213評論 6 29
  • “月朗星稀”,這是很多星月菩提愛好者所極力追求的品質。月孔明朗、方正,星眼稀疏、細小,這是星月菩提子品相優異的體現...
    碎星星_e665閱讀 5,203評論 0 0