Flutter入门——必知必会之Dart基础语法之重载操作符、消息循环和异步小结(三)

引言

前一篇文章Flutter入门——Dart基础语法之类、继承、方法、泛型小结(二)介绍了Dart中方法、类、继承、泛型的相关知识,这篇文章介绍一些高级的知识。

一、重载操作符

使用operator 关键字重载与C++语法差不多。

import "dart:math";

void main(){
  final v1=Vectors(2,4);
  final v2=Vectors(2,6);
  final r1=v1+v2;
  final r2=v1-v2;
  print([r1.x,r1.y]);//[4,10]
  print([r2.x,r2.y]);//[0,-2];
}


class Vectors{
  final int x;
  final int y;
  
  const Vectors(this.x,this.y);
  //重载 +
  Vectors operator +(Vectors v){
    return new Vectors(x+v.x,y+v.y);
  }
  //重载 -
  Vectors operator -(Vectors v){
    return new Vectors(x-v.x,y-v.y);
  }
}

二、异步async和await

Dart 添加了一些新的语言特性用于支持异步编程,Dart 是单线程模型,main方法中的从上往下依次执行,而执行的顺序都是基于不同的事件队列优先级,最典型的是**"dart:async"包下是 async 方法await 表达式,其中await 关键字必须在async方法内部使用且await表达式可以使用多次**。

1、async 方法和await 表达式

在方法体的大括号前使用async 关键字修饰,在方法体内部使用await表达式,所有经过async关键字修饰的方法都会返回一个支持泛型对象的Future< >
在这里插入图片描述
如果在main() 方法里面使用await,main() 的方法体也必须被 async 标记:

main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

三、Dart与消息循环机制

事件循环的任务是从事件队列中一次取出一项并处理且只要队列有内容,就会重复这两步工作。通常消息队列的内容可能是用户输入、文件I/O通知、定时器等等,如下图所示是非Dart语言的一般流程
在这里插入图片描述
而在Dart中虽然类似Handler略有区别,当一个Dart的方法开始执行时,他会一直执行直至达到这个方法的退出点,Dart应用程序在Main Isolate执行main函数之后开始运行,当main函数执行完毕退出后,Main Isolate的线程开始一个个的处理应用程序的事件队列中的内容,下图是Dart出来四个事件的简化流程:
在这里插入图片描述
Dart应用程序仅有一个Event loop消息循环,但存在两个队列:Event事件队列Microtask微任务队列

  • Event loop消息循环——用于循环获取队列中的消息,不断地从消息队列中获取消息进行处理,直至为空。
  • Event事件队列——包含所有的外部事件如I/O、鼠标事件、定时器、Isolate之间的消息等,通过Future 类可以向Event 事件队列尾部添加事件
  • Microtask微任务队列——微任务队列是非常有必要的,因为在返回操作到事件循环前,事件处理代码有时候需要完成某个工作任务,Microtask队列只包含来自当前isolate的内部代码

一个Dart的命令行应用可以通过创建isolates(可以理解为沙箱)来达到并行运行的目的。isolates之间不会共享内存,它们就像几个运行在不同进程中的app中能通过传递message来进行交流。除了明确指出运行在额外的isolates或者workers中的代码外,所有的应用代码都是运行在应用的main isolate中(部分内容翻译自

App启动之后,会从上到下依次执行main方法中的程序语句,当main方法退出后,Event循环就开始它的工作。首先它会以FIFO的顺序从微任务队列中取出执行micro task,当所有micro task执行完后,它才会从event 队列中取事件并执行,如此反复,直到两个队列都为空,所以优先级是 main> 微任务队列>事件队列
在这里插入图片描述
事件循环正在处理micro task的时候,Event 事件队列会被堵塞。此时App就不能绘制任何图形,不能响应鼠标事件和处理文件I/O等事件,因为绘制图形、响应点击事件、处理文件I/O都是在Event 事件队列中处理的。虽然你可以预测任务执行的顺序,但你无法准确的预测到事件循环何时会处理你期望的任务。例如当你创建一个延时1s的任务,但在排在你之前的任务结束前事件循环是不会处理这个延时任务的,也就是说任务执行可能是大于1s的。

1、Future

Future 是一个泛型对象Future< T > 用于表示返回值类型为T的将来的异步操作。如果是返回String 则返回Future< String >,而无返回值则返回Future< void >。重要的是通过Future类可以向Even 事件队列末尾添加一个事件,而处理这个事件要做的事则是通过Future传递进来的

1.1、 立即向Event队列中添加一个任务

new Future(() {
  //实现将来要执行的具体任务代码
});
//以下两个语句,一个上方法体为空,什么都不做,而另一个是参数为空,不存在任何方法。
new Future(() => null);
new Future(null);

1.2、 延时向Event队列中添加一个任务

  // 一秒以后将任务添加至Event队列
  new Future.delayed(const Duration(seconds:1), () {
    //实现将来要执行的具体任务代码
  });

1.3、 then或者whenComplete方法在Future中的任务结束后立刻执行,因为这两个方法本质上只是回调而已没有被放入队列中。

  new Future(() => 9)
      .then((v) => v+9)//自动把Future的一步结果,传递到then中 
      .then((v) => print(v))//自动把then的结果,传递到下一个then中,输出18
      .then((_)=>print(100));//如果不需要传递参数可以使用“ _ ”代替,输出100
      .whenComplete(()=>print(66));//输出100

使用then或者whenComplete在Future结束后会立刻执行某段代码,如果在then()调用之前Future就已经执行完毕了,那么会把对应的操作当成微任务加入到Microtask队列中,而这个任务执行的就是被传入then的方法。whenComplete与then的区别在于即使出现了异常也会被调用到,类似Java中的finally

1.4、Future.value()构造方法会在一个Microtask中完成。

1.5、Future.sync()构造方法会立马执行其参数方法,并在Microtask中完成。

我们通过Future来使用事件队列来调度任务。因为要处理完微任务队列之后才会处理事件队列,所以尽量使用事件队列可以使微任务队列更短,降低事件队列卡死的可能性。

2、使用顶级方法 scheduleMicrotask() 添加到微任务Microtask队列

dart:async定义了一个顶级方法scheduleMicrotask() 用于添加到微任务队列。如果一个任务必须在所有的事件队列之前处理,你应该立即执行该任务,如果不能立即执行,可以使用 scheduleMicrotask()将代码添加到微任务队列。

scheduleMicrotask(() {
  // 实现将来要执行的具体任务代码
});

四、实例讲解

1、Future的一般用法

import 'dart:async';

void main() {
 new Future(()=>futureTask())//异步任务要执行的逻辑
     .then((k)=>'crazy$k')//任务执行完毕后的子任务,每一步的输入就是上一步骤的输出,k就是异步任务的返回值
     .then((kk)=>print('$kk'))//kk为上一步then的返回值
     .then((_)=>new Future.error("出现错误了\n"))
     .whenComplete(()=>print("Finally"))//所有任务执行完毕之后的回调方法
     .catchError((e)=>print("捕获的错误$e"),test:(Object o){
       print("test");//若需要监听完毕这个状态,可以用catchErro 并重写test方法,test方法返回true则可以在onErro方法里捕获异常,反之就捕获不到。
       return true;
      });
}

int futureTask(){
  return 88;
}

未捕获Erro时的结果
在这里插入图片描述
添加捕获Erro语句时的结果
在这里插入图片描述

2、Future (()=>{}) 和Future (()=>{}).then 的链式调用

import 'dart:async';

void main() {
  testFutrues();//f7 f1 f6 f3 f5 f2 f4
}
void testFutrues(){
  print("start enter main");
  Future f= new Future(()=>print("f1"));
  Future f1=new Future(()=>null);
  Future f2=new Future(()=>null);
  Future f3=new Future(()=>null);
  f3.then((_)=>print("f2"));
  f2.then((_){
    print("f3");
    new Future(()=>print("f4"));
    f1.then((_){
      print("f5");
    });
  });
  f1.then((m){
    print("f6");
  });
  print("f7");
  print("end of main");
}

运行结果:
在这里插入图片描述
首先是开始进入main方法,从上往下开始执行,先执行打印语句,接着匿名创建Future f ,缓存到事件队列中(new Future都是放到事件队列中的),接着继续创建Future实例f1、f2、f3,然后中间相当于是注册回调,main方法继续往下执行打印语句,main方法退出,其他未执行的操作都放到其他队列中了,此时事件队列不为空,且微队列为空,开始从上往下处理事件队列,先处理f,由于f中定义了对应的任务再打印出f1,再处理f1,由于f1里没有定义对应的任务紧接着执行then方法,打印f6,再处理f2的任务接着到then方法,在f2的then方法中打印f3,并且创建新的匿名Future 再次缓存到事件队列中的f3之下,并执行f1的then方法,此处由于f1对应的一步任务已经执行完毕,则自动放入到微队列中,由于微队列不为空,所以优先执行微队列的打印f5,微队列又为空了,继续回到事件队列,处理f3的任务紧接着then 打印f2,最后处理在f2的then方法中创建的匿名Future 打印f4
在这里插入图片描述

3、Future 的链式调用和 scheduleMicrotask的优先级关系

3.1

void main(){
  print("start enter main");
  testMicrotask();//s9 s1 s8 s3  s4 s6 s5 s7 s2
  print("end of main");
}
void testMicrotask(){
  scheduleMicrotask(()=>print("s1"));//创建微任务
  new Future.delayed(new Duration(seconds: 2),()=>print("s2"));//具体执行顺序和延时的长短有关
  new Future(()=>print("s3")).then((_){
    print("s4");
    scheduleMicrotask(()=>print("s5"));
  }).then((_)=>print("s6"));
  new Future(()=>print("s7"));
  scheduleMicrotask(()=>print("s8"));
  print("s9");
}

运行结果:
在这里插入图片描述

3.2

如果把3.1的代码在原来的基础的中间增加以下代码,输出结果会是怎么样呢?如果没有理解透肯定很吃惊,输出的顺序竟然是 s9-> s1-> s8-> s3-> s4-> s6-> s5-> s10-> s7->s11-> s12->s2,惊不惊喜意不意外!前面9183465没啥说的,主要讲解下为什么7会比12先输出?原因在于Future是采取了类似链式调用的机制,这意味着只有前面一个节点的任务全部执行完毕之后,下一个节点才会去执行,而后面添加的FutureX 这段,当前面输出完9183465之再次从事件队列中拿出Future 打印s10 之后,第一个then 开始执行此时又创建了将来打印S11一个新的Future 缓存到事件队列中,注意但是仅仅是缓存到事件队列中(此时打印s7的Future 在main第一次执行的时候已经被缓存在事件队列中了,因此这个新的队列只能排在s7的后面),第一个then 的任务还未全部完成,因此第二个then 无法接着执行;经过Event Loop从事件队列中拿到s7,然后再拿到s11的,最后触发FutureX的第二个then 打印s12,由于延时过长最终输出s2。

void testMicrotask2(){
  scheduleMicrotask(()=>print("s1"));//创建微任务
  new Future.delayed(new Duration(seconds: 2),()=>print("s2"));//这其实也有坑,并不是因为delay就一定会放到最后执行的,取决于延时的时间和其他Future 所耗费的时间长短
  new Future(()=>print("s3")).then((_){
    print("s4");
    scheduleMicrotask(()=>print("s5"));
  }).then((_)=>print("s6"));

  /**
  *这里有个隐藏的坑 为了说明把这段叫做FutureX,如果看成链式调用的话 类似这样的结构 print("s10")——>then(new Future...)——>then(print("s12")
  *首先把这三个节点看成一个整体,main第一次执行的时候,会把这个整体缓存到事件队列中,里面的三个子任务均没有被执行,
  直到Event Loop 从事件队列中读取的时候,才会去真正执行里面的任务,而第二个节点又可以看成包含两个子任务new Future 和将来的任务,
  只有这两个任务都执行完毕之后才会触发第二个then执行
  */
  new Future(()=>{print("s10")})
  .then((_)=>new Future(()=>print("s11")))
  .then((_)=>print("s12"));

  new Future(()=>print("s7"));
  scheduleMicrotask(()=>print("s8"));
  print("s9");
}

运行结果:在这里插入图片描述
虽然前面所说的是微任务队列优先级比事件队列高,但是并不意味着,微任务队列可以有无限大的权利抢夺资源,如果事件队列的一个事件正在被执行,即使执行过程中缓存微任务到微任务队列,微任务队列也不会马上被执行,而是在下一次Event Loop 读取队列的时候具有优先级。(以上仅个人观点,仅供参考理解。)

五、调度任务的一般规则

  • 尽量使用事件队列(new Future() 或new Future.delayed())
  • 当需要指定任务顺序的时候,使用Future.then()或whenComplete()
  • 为避免事件队列“饥饿锁死”,尽量使微任务简短
  • 为使应用程序保持响应,避免在事件循环中添加计算密集型代码
  • 执行计算密集型代码的时候,另创建Isolate或Worker

六、生成器Generators

Dart 支持通过生成器Generators延迟创建一系列的值。(When you need to lazily produce a sequence of values, consider using a generator function),而生成器Generators按照生成时机可以分为:同步生成器异步生成器

1、同步生成器Synchronous generator

同步生成器Synchronous generator返回的是 Iterable对象,要实现同步生成器方法,只需要使用sync * 修饰方法体,并在方法体内部使用yield语句来传递值

void main(){
  var iterator =getSyncIterator(4).iterator;//调用此句之后马上返回个Iterable<int>对象,但是内部的方法体还不会执行,
  print("before moveNext");
  //只有调用moveNext方法时getSyncIterator的方法体才会开始执行,
  while(iterator.moveNext()){
    print(iterator.current);
  }

}

Iterable<int> getSyncIterator(int k) sync* {
  print("start");
  int n=k;
  while(n>0){
    yield n--;//同步里yield 会暂停,会等到下一次moveNext()方法执行之后(类似交出同步锁)在执行后面的语句 n--
  }
  print("end");
}

运行结果:
在这里插入图片描述

yield 是ES6新增的关键字,使生成器方法执行暂停,把后面表达式的运算结果返回到生成器的调用者

2、异步生成器Asynchronous generator

异步生成器Asynchronous generator是以Stream 流对象的形式一次性返回的。要实现异步生成器方法,只需要使用async * 修饰方法体,并在方法体内部使用yield语句来传递值

void main(){
  //调用此句之后马上返回个StreamSubscription 对象,通过这个对象可以订阅监听,但是内部的方法体还不会执行,
  StreamSubscription subscription=getAsyncIterator(3).listen(null);
  subscription.onData((value){
    print(value);
    if(value>2){
	   //subscription.pause();//暂停
    }
  });
}

Stream<int> getAsyncIterator(int n) async* {
  print("start");
  int k=0;
  while(k<n){
    yield k++;//yield 在异步中不会暂停,是一次性以流的形式返回
  }
  print("end");
}

运行结果:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/CrazyMo_/article/details/88779757