本文为稀土技术社区首发签约文章,14天内制止转载,14天后未获授权制止转载,侵权必究!
张风捷特烈 – 出品

一、分析 Future 目标

关于 Dart 言语来说,异步运用的过程中,绝大多数场景和 Future 目标有关。C++Java 言语中也有 Future 的概念,关于 JavaScript/Typescript 来说便是 Promise 目标。它们是 异步使命成果 的封装,对 暂未完结 使命的一种 预期(Future) 或 允诺 (Promise)。


1. 认识 Future 目标

前面说过,异步使命有三种状况。在使命分发之后,使命处于未完结状况,而其回来值便是 Future 目标。比方上一篇中运用的文件异步写入办法 writeAsString ,其回来值是 Future<File> 类型。Future 能够指定一个泛型,该类型便是所 等候的成果

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

如下所示,在触发 writeAsString 办法之后,回来值是 一个 对未来的等候。这儿等候回来的类型是 File ,也便是写入后的文件目标。

void saveToFile(TaskResult result) {
  String filePath = path.join(Directory.current.path, "out.json");
  File file = File(filePath);
  String content = json.encode(result);
  Future<File> futureFile = file.writeAsString(content);
}

Future 目标是在使命开端时生成的,认识这一点十分重要。而未来该使命会有什么成果,在 目标诞生时刻 是无法确认的。或许烧水会成功完结,获得 热水 成果;也或许烧水壶爆破,使命失败,获得一个 反常 成果。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

所以 ,关于 Future 目标而言,需求对其进行监听来 感知 使命回调的成果。Future 类中供给了进行监听的,两个十分重要的办法: thencatchError 别离监听 成功完结反常完毕 的场景。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用


2. Future 对使命回调的监听

如下 tag1 处,供给 Future#then 办法,能够监听到使命完结的机遇,并且回调办法中的参数,便是希望的成果数据。

void saveToFile(TaskResult result) {
  String filePath = path.join(Directory.current.path, "out.json");
  File file = File(filePath);
  String content = json.encode(result);
  Future<File> futureFile = file.writeAsString(content);
  futureFile.then((File file) { // tag1
    print('写入成功:${file.path}');
  });
}

下面来看反常情况,比方下面的 saveToErrorFile 办法,没有对应的文件夹,在写入文件时就会发生反常。经过 catchError 办法能够监听使命反常完毕的机遇,对反常成果进行处理。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

void saveToErrorFile(TaskResult result) {
  String filePath = path.join(Directory.current.path, "error","out.json");
  File file = File(filePath);
  String content = json.encode(result);
  Future<File> futureFile = file.writeAsString(content);
  // 监听使命成功完结
  futureFile.then((File file) {
    print('写入成功:${file.path}');
  });
  // 监听使命反常完毕
  futureFile.catchError((err) { 
    print("catchError:$err");
  });
}

假如有些逻辑 无论使命胜败 都需求履行,在 thencatchError 都有要进行书写,比较费事。这样的场景下,能够经过 whenComplete 来监听使命完结机遇。如下日志能够看出,即使使命反常完毕,也能够触发 whenComplete 回调逻辑。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile.whenComplete((){
  print("=======Complete=======");
});

3. 思考异步使命的反常抓取

因为这三个办法都会回来当前 Future 目标,在形式上能够连写。不知道你有没有发现这有点像啥?

futureFile.then((File file) {
  print('写入成功:${file.path}');
}).catchError((err) {
  print("catchError:$err");
}).whenComplete((){
  print("=======Complete=======");
});

没错,这和代码的反常抓取十分像,whenComplete 用于无论如何都会履行的逻辑块,和 finally 代码块殊途同归;catchError 便是抓取反常信息,和 catch 代码块作用相同;then 用于使命履行成功的逻辑处理,和 try 代码块十分神似。

try...catch...finally

其实细心想想,无论是同步还是异步,任何使命都有出错的或许。关于同步使命而言,比方类型转化反常、数值解析反常、分母为 0 反常等。假如预判当前逻辑中或许存在反常情况,需求经过 try...catch 来抓取处理。

同理,关于异步使命而言,本机体仅仅进行 使命分发, 使命真正的履行过程在其他机体中。其他机体只能经过 回调 和本机体通讯,所以关于异步使命而言天然需求回调来处理反常。

别的,关于异步使命而言,出现反常的或许性更大,因为其他机体的处理流程是不行控的。比方网络恳求获取数据,需求 经过网络发送恳求服务器处理恳求服务器发送响应 ,其间每个环节都或许出错,所以对反常的抓取对错常有必要的。


二、深化认识异步反常抓取

1. catchError 中 onError 的细节

Future#catchError 办法源码注释中有相关说明:这儿的榜首参 onErrorFunction 类型,能够是红框中的两类函数:支撑 一参两参

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

如下日志所示,第二参是 _StackTrace 目标,能够依据它定位到出错代码的位置:

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile.catchError((err,stack) {
  print("catchError::[${err.runtimeType}]::$err");
  print("stack at ::[${stack.runtimeType}]::$stack");
});

2. 认识 FutureOr 目标

从上面能够看出 onError 办法需求回来一个 FutureOr 目标。如下,通不经过 then 处理,直接运用 futureFile 目标监听反常,假如不回来,会抛一个 ArgumentError 的反常,可谓 抓一送一

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile.catchError((err){
  print("catchError::[${err.runtimeType}]::$err");
});

已然出反常,天然要处理,所以咱们需求在反常回调办法中,回来一个 FutureOr 目标。


FutureOr 是一个比较特别的目标,在 Dart 源码中它仅仅一个私有结构的抽象类,它不能够被实例化。别的 vm:entry-point 表明它是和虚拟机打交道的。在日常开发中,咱们只需求知道,该类型代表 Future<T>T 类型。也便是说,它是 一类两型 的特别存在。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

@pragma("vm:entry-point")
abstract class FutureOr<T> {
  // Private generative constructor, so that it is not subclassable, mixable, or
  // instantiable.
  FutureOr._() {
    throw new UnsupportedError("FutureOr can't be instantiated");
  }
}

比方 FileFuture<File> 都能够作为 FutureOr 来看待,在 catchError 中能够回来一个 File 目标,或许 Future<File> 异步目标。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile.catchError((err){
  print("catchError::[${err.runtimeType}]::$err");
  return File('this is error file');
});

3. catchError 办法的回来值

经过 catchError 办法的界说能够看出,它能够回来一个 Future<T> 目标。

Future<T> catchError(Function onError, {bool test(Object error)?});

如下,发生反常时,catchError 回来的 futureFile 目标,经过 then 监听时,会打印出 this is error file 这正是 onError 回来的文件目标。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile = futureFile.catchError((err){
  print("catchError::[${err.runtimeType}]::$err");
  return File('this is error file');
});
futureFile.then((value){
  print(value.path);
});

当没有发生反常时 catchError 回来值仍是原先使命目标:

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用


4. catchError 第二参: test

别的 catchError 还有可选的第二参 test,也是一个函数,回来一个 bool 值,入参是 error 目标。test 指定的函数会先于 onError 函数触发,回来值能够控制是否触发 onError

比方下面 test 中假如 error 不是 FileSystemException 才抓取,所以这时榜首参 onError 就不会触发。简略来说 test 用于依据 error 信息,判别是否需求对反常进行抓取,一般来说很少运用,了解一下即可。

futureFile.catchError(
  (err,stack){
    print("catchError::[${err.runtimeType}]::$err");
    print("stack at ::[${stack.runtimeType}]::$stack");
    return File('this is error file');
  },
  test: (error)=> error is! FileSystemException
);

三、 异步成功回调的运用细节

1. then 办法的回来值

咱们一般仅仅在 then 中监听异步使命完结的成果,从下面的 then 办法的界说能够看出:榜首个入参是函数目标,其间函数参数是 T 类中值,也便是 Future 的泛型类型;该函数还有一个回来值,类型为 FutureOr<R> 。其间 R 泛型是办法指定的泛型,then 办法的回来值是 R 泛型的 Future 目标。

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

也便是说,经过 then 办法,能够回来一个其他类型的 Future 目标。如下所示,为 then 办法泛型指定为 String,这样在榜首参函数中回来 FutureOr<String> 目标,then 的回来值便是 Future<String>

咱们知道 File#readAsString 办法回来的是 Future<String>,能够作为榜首参的回来值,这样 then 会回来了一个异步使命 thenResult 。相同能够对这个异步使命运用 then 监听:

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

Future<String> thenResult = futureFile.then<String>((File file) {
  print('写入成功:${file.path}');
  return file.readAsString();
});
thenResult.then((String value){
  print('读取成功:${value}');
});

上面拆开仅仅为了便利了解,如下能够经过连续的 then 调用。假如现在一个异步使命完结后,履行另一个异步使命,这种写法要便利一些。不过一般很少运用,了解一下即可:then 拥有回来另一异步使命的能力。

futureFile.then<String>((File file) {
  print('写入成功:${file.path}');
  return file.readAsString();
}).then((String value){
  print('读取成功:${value}');
});

2. then 办法中的 onError

then 办法有两个入参,榜首个是必传的回调函数,会将 T 类型的数据回调出来。除此之外,还有一个 onError 的可选回调,该过错回调参数也是反常和堆栈信息。

需求留意一点,假如在 then 中供给 onError 回调后,对 then 的回来值再监听 catchError 就会没有作用,如下所示:

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

futureFile.then((File file) {
  print('写入成功:${file.path}');
},onError:(err,stack){
  print("onError::[${err.runtimeType}]::$err");
  print("onError stack at ::[${stack.runtimeType}]::$stack");
}).catchError((err){
  print("catchError:$err");
});

3. whenComplete 办法

最后看一下 whenComplete 办法,该办法的回调中没有任何参数,更像一个 机遇 的监听。表明使命完毕时分需求履行的动作,和 finally 代码块是很类似的。

Future<T> whenComplete(FutureOr<void> action());

留意一下,whenComplete 办法会回来 Future<T> 目标,也便是该异步使命本身,所以你能够连续监听多个 whenComplete 事情。这样,一次异步使命完结后,三个成功事情的监听都会触发,感觉挺有意思的,虽然没太大卵用。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

print("====start Task==${DateTime.now()}===");
Future future = Future.delayed(Duration(seconds: 3));
future.whenComplete((){
  print("====whenComplete1==${DateTime.now()}===");
}).whenComplete((){
  print("====whenComplete2==${DateTime.now()}===");
}).whenComplete((){
  print("====whenComplete3==${DateTime.now()}===");
});

4、 async/await 关键字的运用

经过 then 进行回调,在写法上看起来比较臃肿,特别是当个多个异步使命需求按次序履行时。比方,先读配置文件 config.json;依据配置文件中的信息,读取一个资源文件 a.json ;在读取 a.json 成功之后,将文件中的 read_count 字段 +1

--->[config.json]---
{
  "assets_file_path": "/Users/mac/Coder/Projects/juejin/async_task/config/a.json"
}
--->[a.json]---
{"ip":"198.164.88.001","port":"9090","read_count":11}

当三个异步使命需求依次履行,假如仅经过 then 办法来监听,就会导致回调中嵌套另一个异步使命的回调,让代码看起来很闹心。代码如下:

void readFile(TaskResult result) {
  String filePath = path.join(Directory.current.path,"config","config.json");
  File file = File(filePath);
  Future<String> futureFile = file.readAsString();
  futureFile.then((String value){
    String assetsPath = json.decode(value)['assets_file_path'];
    File file = File(assetsPath);
    file.readAsString().then((String value){
      print(value);
      dynamic map = json.decode(value);
      int count = map['read_count'];
      map['read_count'] = count+1;
      file.writeAsString(json.encode(map)).then((File file){
        print("写入成功:${file.path}");
      });
    });
  });
}

async/await 两个关键字的组合便是为了简化这种场景下的语法书写形式而存在的。语法规则, await 关键字只能在 async 润饰的办法中运用。如下所示,运用 await 关键字润饰 Future 目标后,回来值是成果类型。

代码中 tag1 处运用 await 润饰 Future 目标,表明有必要等候做个异步目标完结后,才干够履行下一行代码。这样代码就能够用同步的办法书写,就像 冲水 需求等候 烧水 使命的完毕,它们在逻辑上是同步的。 但这并不影响 扫地烧水 在逻辑上是异步的。

void readFile(TaskResult result) async{
  String filePath = path.join(Directory.current.path,"config","config.json");
  File configFile = File(filePath);
  String configContent = await configFile.readAsString(); // tag1
  String assetsPath = json.decode(configContent)['assets_file_path'];
  File assetsFile = File(assetsPath);
  String assetsContent = await assetsFile.readAsString();
  print(assetsContent);
  dynamic map = json.decode(assetsContent);
  int count = map['read_count'];
  map['read_count'] = count+1;
  File resultFile = await assetsFile.writeAsString(json.encode(map));
  print("写入成功:${resultFile.path}");
}

await 的价值是简化异步使命完结监听,让依赖于使命成果的后续使命脱节回调监听,从而以一种同步办法更简略地书写。关于某些使命,需求依赖异步使命成果的场景中,运用这两个关键字能够保证功用正确的条件下,让代码的可读性增加。其实这本质上便是个语法糖罢了,认清到使命之间的关系,就很简单了解。


5、 async/await 运用的留意点

或许很多人(包括从前的我)一看到异步办法,就下意识地选择 await 来获取成果,这对错常片面的。await 润饰的异步使命完毕后才会持续向下履行,所以之后的逻辑和使命成果相关时,才有运用的价值。

假如两个异步使命没有关系的话,前一个使命运用 await 润饰,那么后一个使命只能等候前者完毕才干分发。 比方 烧水烧饭 两个异步使命没有什么关系,假如在处理 烧水 时运用 await 等候成果,将 烧饭 使命在下面代码中分发,这显然是对使命的不合理分配。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

所以,在运用 async/await 处理时,要留个心眼:想一下其后的代码是否真的有必要在该使命完结之后才处理;这个 await 的润饰,是否会堵塞到后边不相干的异步使命分发。而不是一味的运用 await 进行处理,这样在某些场景,使命分配不合理,就无法发挥出异步的最大成效。


四、结合运用场景介绍 Future 的运用

Future 作为 Dart 对单个异步使命的封装类,在运用上对错常便利的。掌握了上面的几点常识,能处理日常开发中 90% 对 Future 目标的运用场景。如下是 Future 类的结构图,其间有 6 个结构办法,4 个静态办法,5 个成员办法。一些不常用的功用,这儿暂不介绍,在后期的文章中会做一致介绍。接下来,咱们将结合 Flutter 运用开发,经过 Future 目标进一步地了解异步。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用


1. 场景运用介绍

现在将脱离 Dart 控制台打印,经过 FutureFlutter 运用中的表现对其深化了解。如下所示:在计数器初始项目基础上进行拓宽,点击右下角按钮时,会履行一个异步使命。在异步使命履行的过程中,按钮显现 加载中 作用,且呈灰色不行点击。当使命履行完毕后康复原样,每次异步使命完结之后,会让界面中的数字 +1

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

在开端,先界说一个 TaskState 的枚举,用于标识使命的状况,如下所示: initial 标识初始状况,loading 标识加载中,error 标识使命反常完毕。

enum TaskState {
  initial,
  loading,
  error,
}

这样,咱们能够依据使命运转的状况 TaskState ,对按钮的构建逻辑经过办法进行封装。代码如下所示:逻辑很简略,便是不同的情况下,为 FloatingActionButton 组件供给不同的参数罢了:现在要点便是看初始状况下 _doIncrementTask 办法如何履行异步使命。

Widget buildButtonByState(TaskState state) {
  VoidCallback? onPressed;
  Color color;
  Widget child;
  switch (state) {
    case TaskState.initial:
      child = const Icon(Icons.add);
      onPressed = _doIncrementTask;
      color = Theme.of(context).primaryColor;
      break;
    case TaskState.loading:
      child = const CupertinoActivityIndicator(color: Colors.white);
      color = Colors.grey;
      onPressed = null;
      break;
    case TaskState.error:
      child = const Icon(Icons.refresh);
      color = Colors.red;
      onPressed = renderLoaded;
      break;
  }
  return FloatingActionButton(
    backgroundColor: color,
    onPressed: onPressed,
    child: child,
  );
}

2. Future.delayed 创立延时异步使命

Future.delayed 结构能够创立一个推迟的异步使命。因为榜首入参能够指定使命耗费的时长,这经常被用于一些异步场景的模仿。其间 renderLoading 办法运用更新界面,显现 加载中 的作用,也便是将 _state 置为 TaskState.loading 再触发更新。renderLoaded 办法将界面置为初始状况。

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

从上面能够看出加载中的动画依然在运转中,所以异步的等候并不会堵塞界面线程。经过 await 关键字能够等候异步使命完毕,再履行接下来的代码,使 _counter 自加,更新界面。从这三个使命,我们能够自己品味一下,异步的意义。

int _counter = 0;
TaskState _state = TaskState.initial;
void _doIncrementTask() async {
  renderLoading();
  // 模仿异步使命
  await Future.delayed(const Duration(seconds: 2));
  _counter++;
  renderLoaded();
}
void renderLoading() {
  setState(() {
    _state = TaskState.loading;
  });
}
void renderLoaded() {
  setState(() {
    _state = TaskState.initial;
  });
}

3. 直观感触同步和异步使命的差异

或许有人还是体会不出,下面改一下代码,让你更直观的感触两者之间的距离。与 异步等候 相对应的是 同步等候,运用 sleep 办法能够模仿同步等候的耗时使命。如下,将代码改为同步等候两秒,能够看出线程被堵塞,在此期间程序就无法做出任何反响,俗称 "卡死"

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

void _doIncrementTask() async {
  renderLoading();
  // 模仿同步等候使命耗时
  sleep(const Duration(seconds: 2));
  _counter++;
  renderLoaded();
}

经过同步和异步等候的对比,想必你应该理解两者的距离。上面的 sleep 办法,在日常开发中就对应一下需求在 Dart 代码中处理的核算密集型的使命,比方下面的 loopAdd 办法,履行十亿次的累加核算,便是在同步履行一个耗时使命,卡住是必定的。当然也有处理的计划,在后期文章中会进行讨论。

void _doIncrementTask() async {
  renderLoading();
  loopAdd(1000000000);
  _counter++;
  renderLoaded();
}
int loopAdd(int count) {
  int sum = 0;
  for (int i = 0; i <= count; i++) {
    sum+=i;
  }
  return sum;
}

3. Future.delayed 的第二参运用

很多人或许只知道 Future.delayed 仅仅作为异步延时一下,并没有介意它的第二参。如下所示,第二参数是提个函数,无回调参数,回来 FutureOr<T> 目标。也便是说 Future.delayed 也能够有异步使命的成果值。

Future.delayed(Duration duration, [FutureOr<T> computation()?])

Future.delayed 也能够模仿异步使命的回来成果、使命反常的情况。如下所示:当 counter 自加之后是 3 的倍数时,抛出反常。经过 computation 函数就能够完成:

【Flutter 异步编程 - 贰】 |  详细分析 Future 类的使用

代码如下:界说一个 computation 函数作为第二入参(函数名可任意指定),在其间对 counter % 3 == 0 时,经过 throw 抛出反常,来模仿异步使命的反常情况。假如没有反常,则回来 counter 值,该值将作为延时异步使命的成果值。

因为这儿运用 await 关键字,之后的逻辑能够看作同步履行的,能够运用 try...catch 来抓取反常。

void _doIncrementTask() async {
  renderLoading();
  // 模仿异步使命
  try {
    _counter = await Future.delayed(const Duration(seconds: 1), computation);
    renderLoaded();
  } catch (e) {
    renderError();
  }
}
FutureOr<int> computation(){
  int counter = _counter + 1;
  if( counter % 3 == 0 ){
    throw 'error';
  }
  return counter;
}
void renderError() {
  setState(() {
    _state = TaskState.error;
  });
}

本文详细介绍了 Future 目标的运用,需求额外留意三个回调办法的运用办法,以及 async/await 关键字的运用场景。别的结合 Flutter 中的一个小运用案例,体会了一些异步在实际开发中的运用办法。不过 Future 仅是异步编程的一部分, 在某些场景下 Future 有其局限性,咱们需求一种更高档的手法来处理,下一篇咱们将进入流 Stream 的认知,敬请等候。那本就到这儿,谢谢观看 ~

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。