(五)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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 以下内容从官网得到:https://webdev.dartlang.org/articles/performanc...
    None_Ling阅读 37,033评论 7 34
  • Mixin 备注:子类没有重写超类A方法的前提下,如果2个或多个超类拥有相同签名的A方法,那么子类会以继承的最后一...
    小流星雨阅读 1,340评论 0 2
  • 《The Event Loop and Dart》译文 原文:https://webdev.dartlang.or...
    无玄阅读 867评论 0 2
  • 前言 我们所熟悉的前端开发框架大都是事件驱动的。事件驱动意味着你的程序中必然存在事件循环和事件队列。事件循环会不停...
    HowHardCanItBe阅读 15,169评论 6 29
  • “月朗星稀”,这是很多星月菩提爱好者所极力追求的品质。月孔明朗、方正,星眼稀疏、细小,这是星月菩提子品相优异的体现...
    碎星星_e665阅读 5,104评论 0 0