Dart学习笔记

196 阅读10分钟

语法

变量和常量

指定类型

  • var 优先使用 var : 以初始值决定变量类型,后续不可更改类型
var a = "hello"
  • 以类型作为关键字
String a = "hello"

非指定类型

使用 dynamic 或者 Object 来指定类型,后续可以更改值类型,后续使用值时需要进行类型转换,

  • Object
Object a = "hello"
a = 8 // 不报错
  • dynamic
dynamic a = "hello"
a = 8 // 不报错

修饰符

  • var 变量
  • final 变量只可以被赋值一次
  • const 编译时常量,同时也是final类型

实例变量可以是final但是不可以是const

字符串和数字互相转换

String转换成Number类型,使用Number.parse(string)方法,例如:

// String -> int
var one = int.parse('1');
// String -> double
var onePointOne = double.parse('1.1');

Number转换成String类型,使用Number.toString()方法,例如:

// int -> String
String oneAsString = 1.toString();

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);

double转string时,可以使用double.toStringAsFixed(2)来决定保留几位小数;toStringAsFixed有如下规则:

  • 四舍五入
  • 不足的小数点位数补0

String大小写转换:

String hl = 'helloWorld';
var lo = hl.toLowerCase(); // helloworld
var up = hl.toUpperCase(); // HELLOWORLD

int和double转换:

double d = 3.6;
var i = d.toInt(); // 3
double dd = i.toDouble(); //3,但是是double类型

toInt()取整时,是按退一法处理,直接舍弃了小数位,所以3.6和3.4,取整的值都是3。

函数

函数也是对象,类型为 Function.

写法

常规:

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

显式声明返回类型为bool

箭头函数: => { return 表达式; }

函数只包含一个表达式时,可以简写:

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

后面只能跟表达式,不能跟其它语句。

参数

分为必要参数可选参数,必要参数定义在参数列表前面,可选参数则定义在必要参数后面。

必要参数

void enableFlags(bool? bold, bool? hidden) {...}

必要参数,调用函数时,bold和hidden两个参数都是必传,不可以省略。

enableFlags(bold: true, hidden: false);

//下面这样写会编译失败
enableFlags(bold: true);

可选参数 {}

{}将参数包裹,这样调用函数时,可以省略不需要传入的参数

void enableFlags({bool? bold, bool? hidden}) {...}

可选参数可以不传:

enableFlags(bold: true, hidden: false);

//下面这样写编译成功
enableFlags(bold: true);

可选参数的写法,无论是{}还是[],都必须使用可为空的参数?,或者是给定默认值:

void enableFlags({bool? bold, bool hidden = false}) {...}
命名参数

命名参数默认为可选参数,除非他们被特别标记为 required。required参数不可为空

const Scrollbar({Key? key, required Widget child})

标记了required的参数,如果调用函数时不传就会编译失败。

可选的位置参数 [参数1, 参数2]

这个一般不会用到,起到和可选参数一样的效果:

void enableFlags(bool? bold, [bool? hidden]) {...}

调用时不传hidden参数不会报错。

enableFlags(bold: true, hidden: false);

//下面这样写编译成功
enableFlags(bold: true);

匿名函数(闭包)

var func = (){
    print("我是匿名方法");
  };
  func();

使用闭包时,需要注意内存泄露。

运算符

赋值运算符

为空赋值 ??=

如果变量为空则赋值,不为空则不赋值:

String? a
a ??= "hehe" // hehe
a ??= "xixi" // 还是hehe

类型判断运算符

类型检查isis!

检查变量是否是指定的类型:

  • is 如果对象是指定类型则返回 true
  • is! 如果对象是指定类型则返回 false
Object i = 3; 

if (i is int) {
	...
}

类型转换as

仅当你确定这个对象是该类型的时候,你才可以使用 as 操作符可以把对象转换为特定的类型;否则会抛出异常

Object i = 3; 

if (i is int) {
	int m = i as int
}

级联运算符 .., ?..

级联运算符 可以让你在同一个对象上连续调用多个对象的变量或方法。

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

等价于:

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

如果操作的对象可以为空,需要使用?..级联:

Paint? paint = Paint()
  ?..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

等价于:

Paint? paint = Paint();
paint?.color = Colors.black;
paint?.strokeCap = StrokeCap.round;
paint?.strokeWidth = 5.0;

使用级联可以减少创建变量的步骤

判空 ?!

上面的例子:

Paint? paint = Paint();
paint?.color = Colors.black;
paint?.strokeCap = StrokeCap.round;
paint?.strokeWidth = 5.0;

paint?.color使用?进行判空,如果是空则不会赋值,非空时才会对color进行赋值; 如果使用!进行强制非空判断,paint!.color = Colors.black;那么如果paint为null,则会抛出运行时异常。

流程控制语句

switch

Switch 语句在 Dart 中使用 == 来比较整数、字符串或编译时常量,比较的两个对象必须是同一个类型且不能是子类并且没有重写 == 操作符。

  • 非空case需要加break
  • 空case默认 fall-through
  • 非空case可以使用 continue 来实现fall-through。
var command = 'CLOSED';
switch (command) {
  case 'OPEN': //直接走到下一个case CLOSED
  case 'CLOSED':
    executeClosed();
    continue nowClosed;
  // 从nowClosed标签那里继续.

  nowClosed:
  case 'NOW_CLOSED':
    // Runs for both CLOSED and NOW_CLOSED.
    executeNowClosed();
    break;
}

异常

throw 抛出异常

优秀的代码通常会抛出 ErrorException 类型的异常。

throw FormatException('Expected at least 1 section');
throw Error();

Exception 旨在向用户传递关于故障的信息,以便可以通过编程方式解决错误。它旨在被捕获,并且它应该包含有用的数据字段。

Error 表示程序员应该避免的程序错误.

try-catch 捕获异常

捕获异常可以避免异常继续传递(重新抛出异常除外)。捕获一个异常可以给你处理它的机会:

  • on 指定异常类型
  • catch 来捕获异常对象,throw抛出的对象,catch(err)捕获
  • rethrow 捕获的异常再次抛出
  • finally 无论是否抛出异常,finally 语句始终执行,如果没有指定 catch 语句来捕获异常,则异常会在执行完 finally 语句后抛出.
try {
  //执行操作
} on OutOfLlamasException { // on 捕获特定类型的异常
  //do something
} on Exception catch (e) { // 使用 on 来指定异常类型,使用 catch 来捕获异常对象, 两者可同时使用
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) { // 使用 catch 来捕获异常对象
  // No specified type, handles all
  print('Something really unknown: $e');
  rethrow; //可以使用rethrow将捕获的异常再次抛出
} finally { // 无论是否抛出异常,finally 语句始终执行
  // Always clean up, even if an exception is thrown.
}

捕获Future.error()异常

如果是在异步方法async方法里面,可以使用try-catch来捕获,Future需要使用await 关键字.

void main() async {
  try {
    await request();
  }
  catch (e) {
   print("catch error: ${e.toString()}");
  }
}
Future request() {
  return Future.error(FormatException("test"));
}

输出结果:

catch error: FormatException: test

如果不使用await关键字,try-catch无法捕获,异常会被抛出:

void main() async {
  try {
    request();
  }
  catch (e) {
   print("catch error: ${e.toString()}");
  }
}
Future request() {
  return Future.error(FormatException("test"));
}

输出结果:

Uncaught Error: FormatException: test

runtimeType 获取对象的类型

print('The type of a is ${a.runtimeType}');

构造函数

声明一个与类名一样的函数即可声明一个构造函数

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    // See initializing parameters for a better way
    // to initialize instance variables.
    this.x = x;
    this.y = y;
  }
}

dart简写方式:

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);
}

命名式构造函数

可以为一个类声明多个命名式构造函数来表达更明确的意图:

const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  //普通构造函数
  Point(this.x, this.y);

  // 命名式构造函数
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

初始化列表

除了调用父类构造函数之外,还可以在构造函数体执行之前初始化实例变量。每个实例变量之间使用逗号分隔。

Point.fromJson(Map<String, double> json)
    : x = json['x']!,
      y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

⚠️:初始化列表表达式 = 右边的语句不能使用 this 关键字。

重定向构造函数

有时候类中的构造函数仅用于调用类中其它的构造函数,此时该构造函数没有函数体,只需在函数签名后使用:指定需要重定向到的其它构造函数 (使用 this 而非类名):

class Point {
  double x, y;

  //主构造函数
  Point(this.x, this.y);

  // 重定向到主构造函数.
  Point.alongXAxis(double x) : this(x, 0);
}

常量构造函数

如果类生成的对象都是不变的,可以在生成这些对象时就将其变为编译时常量。你可以在类的构造函数前加上 const 关键字并确保所有实例变量均为 final 来实现该功能。

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);
  // 常量构造函数需要使用final关键字
  final double x, y;
  // 常量构造函数
  const ImmutablePoint(this.x, this.y);
}

⚠️:常量构造函数创建的实例并不总是常量

工厂构造函数⚠️

使用 factory 关键字标识类的构造函数将会令该构造函数变为工厂构造函数,这将意味着使用该构造函数构造类的实例时并非总是会返回新的实例对象。

例如,工厂构造函数可能会从缓存中返回一个实例,或者返回一个子类型的实例。

class Logger {
  final String name;
  bool mute = false;

  // _cache是库私有的,因为它的名字前面有一个'_'
  static final Map<String, Logger> _cache = <String, Logger>{};

  //工厂构造函数,从缓存里面取对象实例
  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }
  //工厂构造函数,返回默认构造函数创建的实例
  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  //私有的构造函数,使用_开头的方法和变量是私有的意思,等同于private
  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

⚠️在工厂构造函数中无法访问 this

方法

重写操作符 operator

注意写法:

class Vector {
  final int x, y;

  Vector(this.x, this.y);

  //重写+操作符
  Vector operator +(Vector v) => Vector(x + v.x, y + v.y);
  //重写-操作符
  Vector operator -(Vector v) => Vector(x - v.x, y - v.y);

}
void main() {
  final v = Vector(2, 3);
  final w = Vector(2, 2);

  assert(v + w == Vector(4, 5));
  assert(v - w == Vector(0, 1));
}

Getter 和 Setter

实例对象的每一个属性都有一个隐式的 Getter 方法,如果为非 final 属性的话还会有一个 Setter 方法.

注意getset关键字的写法:

class Rectangle {
  double left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // 定义两个计算属性,right和bottom

  double get right => left + width;
  set right(double value) => left = value - width;

  double get bottom => top + height;
  set bottom(double value) => top = value - height;
}

void main() {
  var rect = Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}

重写get和set方法时,属性不能是存储属性,只能是计算属性;除非再定义一个变量去接收它。

  double _right = 0;
  double get right => _right;
  set right(double value) => _right = value;

抽象方法

定义一个接口方法而不去做具体的实现让实现它的类去实现该方法,抽象方法只能存在于 抽象类中。

实例方法、Getter 方法以及 Setter 方法都可以是抽象的.

// 抽象类 关键字abstract
abstract class Doer {
  //抽象方法,没有{}方法体,用;结尾
  void doSomething(); 
}
// 实体类继承自抽象类,需要实现抽象方法
class EffectiveDoer extends Doer {
  // 抽象方法的实现
  void doSomething() {
    // Provide an implementation, so the method is not abstract here...
  }
}

抽象类 abstract (需要注意和协议Mixin的区别)

使用关键字 abstract 标识类可以让该类成为 抽象类,抽象类将无法被实例化。

abstract class Doer {
  //抽象方法,没有{}方法体,用;结尾
  void doSomething(); 
  //抽象类可以有具体方法
  void real() {
  	print("hahahah");
  }
}

class EffectiveDoer extends Doer {
  // 抽象方法的实现
  void doSomething() {
    print("dosomething");
  }
}


void main() {
  var ef = EffectiveDoer();
  ef.real(); //hahahah
  ef.doSomething(); //dosomething
}

隐式接口 implements

每一个类都隐式地定义了一个接口并实现了该接口,这个接口包含所有这个类的实例成员以及这个类所实现的其它接口。如果想要创建一个 A 类支持调用 B 类的 API 且不想继承 B 类,则可以实现 B 类的接口。

// 定义person类,包含一个打招呼的greet方法
class Person {
  // 姓名字段
  final String _name;

  // 构造函数
  Person(this._name);

  // 打招呼
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// 定义另一个冒充类,实现Person的打招呼方法
class Impostor implements Person {
  //如果要隐式实现Person的方法,需要也创建对应的name属性,不然会报错。
  String get _name => '';
  //实现Person的方法
  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy'))); //Hello, Bob. I am Kathy.
  print(greetBob(Impostor())); //Hi Bob. Do you know who I am?
}

如果需要实现多个类接口,可以使用逗号分割每个接口类:

class Point implements Comparable, Location {...}

使用implements关键字之后,等于需要全盘实现右边类的属性和方法,不然就会编译错误。 隐式接口和抽象类的区别:一个类只能有一个父类,但是可以有多个隐式接口;继承同样可以调用父类的方法。

子类 extends

使用 extends 关键字来创建一个子类,并可使用 super 关键字引用一个父类:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
}

class SmartTelevision extends Television {
  void turnOn() {
  	//super关键字调用父类方法
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ···
}

重写类成员 @override

//动物
class Animal {
  //定义捕猎方法
  void chase(Animal x) { ... }
}
//老鼠
class Mouse extends Animal { ... }
//猫
class Cat extends Animal {
  @override
  //猫重写捕猎方法
  void chase(Animal x) { ... }
}
协变关键字 covariant

上面的例子里,猫是捕猎老鼠的,但是在重写方法时,如果直接这样写:

class Cat extends Animal {
  @override
  void chase( Mouse x) {  } //编译不通过,无法修改入参类型
}

这个时候,就可以使用covariant关键字,来收紧参数类型: class Cat extends Animal { //猫捕猎老鼠 @override void chase(covariant Mouse x) { } //编译通过 }

covariant关键字只能用来收紧类型,比如收紧成原参数的子类,不能转换成其它类型。

拓展方法 extension

拓展和swift差不多,可以拓展方法和计算属性,不能拓展存储属性。

// extension 拓展名 on 类名
extension NumberParsing on String {
  //拓展方法
  int parseInt() {
    return int.parse(this);
  }
  // ···
}

拓展支持泛型

extension MyFancyList<T> on List<T> {
  int get doubleLength => length * 2;
  List<T> operator -() => reversed.toList();
  List<List<T>> split(int at) => [sublist(0, at), sublist(at)];
}

API 冲突

假设有三个文件

  • string_apis.dart 定义 NumberParsing的String拓展,实现了 parseInt()方法
  • string_apis_2.dart 定义 NumberParsing2的String拓展,实现了 parseInt()方法
  • string_apis_3.dart 定义 NumberParsing的String拓展,实现了 parseInt()方法

三个文件的 parseInt()方法冲突了,可以用以下几种方式解决:

使用showhide

更改导入冲突扩展的方式,使用showhide限制公开的 API:

import 'string_apis.dart';
// 导入string_apis_2但是隐藏其中的 NumberParsing2拓展
import 'string_apis_2.dart' hide NumberParsing2;

这样有个写起来比较方便,但是 NumberParsing2里面其它的方法也会被隐藏。

显式应用扩展
import 'string_apis.dart'; // 正常导入.
import 'string_apis_2.dart'; // 正常导入.

// print('42'.parseInt()); // 无法工作

// 用拓展名包裹它的拓展类型,来区分域
print(NumberParsing('42').parseInt());
print(NumberParsing2('42').parseInt());

这会导致代码看起来好像扩展是一个包装类。

如果两个扩展名相同,那么可能需要使用前缀导入:

import 'string_apis.dart';
import 'string_apis_3.dart' as rad; //给导入的文件取个前缀rad

print(NumberParsing('42').parseInt());

// 在使用时,通过前缀去调用
print(rad.NumberParsing('42').parseInt());

枚举 enum

使用关键字 enum 来定义枚举类型:

enum Direction { horizontal, vertical }

枚举取值是成员变量index,值是整型,从0开始。不像swift那样可以使用其他参数作为枚举值。

枚举不能成为子类,也不可以 Mixin,你也不可以实现一个枚举。 不能显式地实例化一个枚举类。

Mixin类 with

mixin Walk {
  int legs = 2;
  void walk() {
    print("walk with $legs legs");
  }
}

mixin Eat {
  // mixin里面可以使用抽象类
  void eat();
}

class Animal with Walk, Eat  {
  // 需要@override关键字
  @override
  int legs = 4; 
  @override
  void eat() {
    print("eat somthing");
  }
}
class Person with Walk, Eat  {
 @override
  void eat() {
    print("eat animal");
  }
}
class Cart extends Animal { 
  @override
  void eat() {
    print("eat fish");
  }
}

void main() {
  var person = Person();
  person.eat(); //eat animal
  person.walk(); //walk with 2 legs
  var animal = Animal();
  animal.eat(); //eat somthing
  animal.walk(); //walk with 4 legs
  var cat = Cart();
  cat.eat(); //eat fish
} 

特性:

  • 可以多继承
  • 可以使用抽象方法
  • 需要使用@override关键字重写

类变量和方法 static

和其他语言差不多,使用 static 关键字。

异步支持

asyncawait关键词支持了异步编程,允许您写出和同步代码很像的异步代码。

Future 单次调用链

Future与JavaScript中的Promise非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。

从swift了里面找的话,就和Result<success,error>相近,不过Result只是表示一个结果的对象。从功能和写法上讲,更像RXSwift里面的一次性序列对象,可能发出一个成功信号(then类比onNext),也可能发出一个失败信号(catchError类比onError),无论成功失败都会有完成信号(whenComplete类比onCompleted),也仅仅只有写法类似,其它的原理什么的都不一样。

Future

Future可以在<>里指定成功后返回参数的类型,和Result<success,error>,和Observable<Element>写法差不多

Future.then 接收结果

Future.delayed(new Duration(seconds: 2),(){
   return "hi world!";
}).then((data){
   print(data);
});

Future.catchError 接收错误

Future.delayed(new Duration(seconds: 2),(){
   //return "hi world!";
   throw AssertionError("Error");  
}).then((data){
   //执行成功会走到这里  
   print("success");
}).catchError((e){
   //执行失败会走到这里  
   print(e);
});

Future.then里的可选参数onError也能接收错误

Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
}).then((data) {
    print("success");
}, onError: (e) {
    print(e);
});

Future.whenComplete 接收完成事件,无论成功失败都会走

Future.delayed(new Duration(seconds: 2),(){
   //return "hi world!";
   throw AssertionError("Error");
}).then((data){
   //执行成功会走到这里 
   print(data);
}).catchError((e){
   //执行失败会走到这里   
   print(e);
}).whenComplete((){
   //无论成功或失败都会走到这里
});

Future.wait 异步等待多个Future

Future.wait([
  // 2秒后返回结果  
  Future.delayed(new Duration(seconds: 2), () {
    return "hello";
  }),
  // 4秒后返回结果  
  Future.delayed(new Duration(seconds: 4), () {
    return " world";
  })
]).then((results){
  print(results[0]+results[1]);
}).catchError((e){
  print(e);
});

如果Future.wait里面的Future数组返回值类型统一,如上例,那么then方法里面的results的类型也是统一的(JSArray<String> ),如果不统一,则是JSArray<Object>

Async/await 异步

task() async {
   try{
    String id = await login("alice","******");
    String userInfo = await getUserInfo(id);
    await saveUserInfo(userInfo);
    //执行接下来的操作   
   } catch(e){
    //错误处理   
    print(e);   
   }  
}

async

用来表示函数是异步的,定义的函数会返回一个Future对象,可以使用 then 方法添加回调函数。

await

后面是一个Future,表示等待该异步任务完成,异步完成后才会往下走;await必须出现在 async 函数内部。

async/await 只是一个语法糖,编译器或解释器最终都会将其转化为一个 Promise(Future)的调用链 调用 async 方法时,如果不加 await ,则代码会立刻往下执行,不会等待异步结果返回 await只能出现在异步方法里面,如果要在同步方法调用,则使用Future.then

Stream 流式调用链

Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。

Stream每次返回的结果没有相关性,不会因为一次返回失败而导致后续全部终止; Stream.listen可以接收不同返回类型的Future的值 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。

Stream.fromFutures([
  // 1秒后返回结果
  Future.delayed(Duration(seconds: 1), () {
    return "hello 1";
  }),
  // 抛出一个异常
  Future.delayed(Duration(seconds: 2),(){
    throw AssertionError("Error");
  }),
  // 3秒后返回结果
  Future.delayed(Duration(seconds: 3), () {
    return 1;
  })
]).listen((data){
   print(data);
   print(data.runtimeType);
}, onError: (e){
   print(e.message);
},onDone: (){

});

机制

Event Loop 机制

Main isolate 中有一个Looper,但存在两个Queue: Event QueueMicrotask Queue:

流程图

dart 代码执行的顺序: Main > Microtask > Event 队列的执行顺序:先进先出

Microtask Queue => scheduleMicrotask

微任务的优先级高,优先完成微任务,才去执行Event任务;

scheduleMicrotask(() => print('This is a microtask'));

适用手势识别、文本输入、滚动视图、保存页面效果等需要高优执行任务的场景

Event Queue => Future

then 与 Future 函数体共用一个事件循环

Future函数体执行完成后,会立马执行then函数体,整个完成后,才算走完一个事件循环.

如果 Future 执行体已经执行完毕了,但你又拿着这个 Future 的引用,往里面加了一个 then 方法体,Dart 会将后续加入的 then 方法体放入微任务队列,尽快执行.

如果Future 函数体是 null,这意味着它不需要也没有事件循环,Dart 会把后续的 then 放入微任务队列,在下一次事件循环中执行.


Future(() => print('f1'));// 声明一个匿名 Future
Future fx = Future(() =>  null);// 声明 Future fx,其执行体为 null; 执行完成后会立马执行then
 
// 声明一个匿名 Future,并注册了两个 then。在第一个 then 回调里启动了一个微任务
Future(() => print('f2')).then((_) {
  print('f3');
  //此时再增加一个微任务队列,但是因为then方法和Future方法公用一个事件循环,会优先走f5,循环结束后再走f4
  scheduleMicrotask(() => print('f4'));
}).then((_) => print('f5'));

//===注意: 这里的print顺序很容易出错 
// 声明了一个匿名 Future,并注册了两个 then。第一个 then 是一个 Future
Future(() => print('f6'))
  .then((_) => Future(() => print('f7'))) //then1 是一个Future
  .then((_) => print('f8'));
// 声明了一个匿名 Future
Future(() => print('f9'));
//===

// 往执行体为 null 的 fx 注册了了一个 then, 此时then方法也是一个微任务
fx.then((_) => print('f10'));
 
// 启动一个微任务
scheduleMicrotask(() => print('f11'));
print('f12');

输出结果如下:

f12
f11
f1
f10
f2
f3
f5
f4
f6
f9
f7
f8

既然 thenFuture 函数体共用一个事件循环, 为什么这里的代码:

Future(() => print('f6')) // Future1
  .then((_) => Future(() => print('f7')))  // then1 Future2 
  .then((_) => print('f8')); // then2
Future(() => print('f9')); // Future3

输出的顺序是: 6 => 9 => 7 => 8 ? 不应该是 6 => 9 => 8 => 7 吗?

看一下then的代码说明: then方法,每次都会返回一个新的Future<R>对象

  Future<R> then<R>(FutureOr<R> onValue(T value), {Function? onError});

类型 FutureOr<R>,表示可能是Future<R>或者直接是R.

onValue如果是R,那么会和Future1共用一个事件循环.

onValue如果是Future<R>, 则then方法体执行后创建了一个新的Future, 也就是说Future1执行了then1之后,变成了Future2,那么此处then2指向的不再是Future1而是Future2.

所以输出会是 6 => 9 => 7 => 8.

如果改成:

Future(() => print('f6')) // Future1
  .then((_) {
    Future(() => print('f7')) // Future2
  })  // then1  
  .then((_) => print('f8')); // then2
Future(() => print('f9')); // Future3

那么输出顺序变成了6 => 8 => 9 => 7.

执行then1时,没有return新的Future,只是创建了一个新的Future2Event Queue

Isolate 多线程

Dart是单线程语言,只创建了一个main isolate. 其中异步的实现是通过上面的EventLoop机制来实现的.

如果需要使用多线程,充分利用CPU性能,则需要创建新的isolate;

  • 每个 Isolate 都有自己的 Event Loop 与 Queue.
  • Isolate 之间不共享任何资源,只能依靠消息机制通信,因此也就没有资源抢占问题。
  • Isolate 通信依赖管道(ReceivePort).
Isolate isolate;
 
start() async {
  ReceivePort receivePort= ReceivePort();// 创建管道
  // 创建并发 Isolate,并传入发送管道
  isolate = await Isolate.spawn(getMsg, receivePort.sendPort);
  // 监听管道消息
  receivePort.listen((data) {
    print('Data:$data');
    receivePort.close();// 关闭管道
    isolate?.kill(priority: Isolate.immediate);// 杀死并发 Isolate
    isolate = null;
  });
}
// 并发 Isolate 往管道发送一个字符串
getMsg(sendPort) => sendPort.send("Hello");

需要通信时,代码太过复杂,特别是涉及到双向通信时.可以直接使用flutter封装好的并发计算函数compute.

compute 并发计算函数

compute 可以创建一个并发线程来处理复杂的计算,防止阻塞main isolate导致卡顿,比如超大json解析,图片数据处理等场景.但是compute也有条件限制:

  • 传入的方法只能是顶级函数static函数.
  • 只有一个传入参数一个返回值

并且每次调用compute,都相当于创建一个isolate,频繁使用的话,性能消耗也很大.

使用compute前:

// 并发计算阶乘
Future<dynamic> asyncFactoriali(n) async{
  final response = ReceivePort();// 创建管道
  // 创建并发 Isolate,并传入管道
  await Isolate.spawn(_isolate,response.sendPort);
  // 等待 Isolate 回传管道
  final sendPort = await response.first as SendPort;
  // 创建了另一个管道 answer
  final answer = ReceivePort();
  // 往 Isolate 回传的管道中发送参数,同时传入 answer 管道
  sendPort.send([n,answer.sendPort]);
  return answer.first;// 等待 Isolate 通过 answer 管道回传执行结果
}
 
//Isolate 函数体,参数是主 Isolate 传入的管道
_isolate(initialReplyTo) async {
  final port = ReceivePort();// 创建管道
  initialReplyTo.send(port.sendPort);// 往主 Isolate 回传管道
  final message = await port.first as List;// 等待主 Isolate 发送消息 (参数和回传结果的管道)
  final data = message[0] as int;// 参数
  final send = message[1] as SendPort;// 回传结果的管道 
  send.send(syncFactorial(data));// 调用同步计算阶乘的函数回传结果
}
 
// 同步计算阶乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
main() async => print(await asyncFactoriali(4));// 等待并发计算阶乘结果

使用compute后:

// 同步计算阶乘
int syncFactorial(n) => n < 2 ? n : n * syncFactorial(n-1);
// 使用 compute 函数封装 Isolate 的创建和结果的返回
main() async => print(await compute(syncFactorial, 4));