关于 Flutter 的异步编程 2、async 和 await

这是 Flutter 异步编程的第二篇文章。主要讲一下真正项目中真正常用的异步写法。

需要对 Future 对象有一点初步了解,可以看看我之前的一篇:

这里直接开始

响应式编程

响应式编程是在开发中经常使用也很好用的一种编程思想。具体定义就不详细讲了,直接开始。

比如这是一个获取天气数据的一个封装代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class WeatherInfo {

int cityId = 0;
String weather = '晴';

/// 获取天气
/// [cityId] 城市 ID
static Future<WeatherInfo> getWeather(int cityId){
return new Future((){
var result = /*进行网络请求获取天气*/;

return new WeatherInfo()
..cityId = cityId
..weather = result;
});
}
}

当我们调用这个方法的时候,直接异步获取天气数据并返回一个 Future 对象,这样我们在外面直接用 then 承接即可。例如:

1
2
3
4
main(){
WeatherInfo.getWeather(310000)
.then((value) => print('天气为${value.weather}'));
}

这就是一个标准的响应式编程的模型。将所有异步操作都和上面 WeatherInfo 一样封装成一个 Future ,要用的时候直接调用获取并用 then 获取结果。

这里的 Future<WeatherInfo> 可以看到后面带了一个泛型,用来限制 Future 的操作中只能返回这一个类型的值。

回调地狱

我们以上响应式编程的实现方法可以说是传入回调。就如那个 then 的参数函数,实际上就是一个回调。

如果只有一个请求肯定问题不大,但是如果有多个呢?

比如我们在上面的基础上再加一个请求,先联网获取城市 ID(根据城市名字搜索城市专属 ID)。然后在获取天气:

因为联网的方法比较简单,这里直接省略

1
2
3
4
5
6
7
8
9
10
main(){
CityInfo.getCity('汕头市')
.then((cityId){

WeatherInfo.getWeather(cityId)
.then((value) => print(value));

});

}

这里 CityInfo.getCity 假设为响应式编程的根据城市名字获取 城市ID,具体也返回一个 Future 对象,然后我们用 then 承接,然后在结果中再次调用 WeatherInfo.getWeather 获取天气。

可以看出这里传入了两层回调,其实这只是个例子,在真正的开发中,可能遇到不止两层的情况,结果可能是很多层。这种多层回调嵌套的情况称之为回调地狱。

回调地狱 让代码可读性变差。同时,也写了很多重复的代码,比如响应式编程中每次都要 new Future(xx) (虽然 Flutter 支持省略 new),非常繁琐。

所以 dart 就专门为了适配这种情况,使用语法糖关键字来简化代码。就是我们今天的主角 asyncawait

async

关于这个关键字不用过多介绍,直接上效果,以下两个代码是等效的:

1
2
3
4
5
6
7
8
9
10
11
12

/// 获取天气
/// [cityId] 城市 ID
Future<WeatherInfo> getWeather(int cityId){
return new Future((){
var result = /*进行网络请求获取天气*/;

return new WeatherInfo()
..cityId = cityId
..weather = result;
});
}
1
2
3
4
5
6
7
8
9
/// 获取天气
/// [cityId] 城市 ID
Future<WeatherInfo> getWeather(int cityId) async{
var result = /*进行网络请求获取天气*/;

return new WeatherInfo()
..cityId = cityId
..weather = result;
}

没错,一个方法如果返回值是 Future 对象,然后参数后面带有 async 关键字的,系统会自动将代码里所有代码都包装到一个 Future 中,并返回。

await

也是一样,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
main(){
printWeather();
}

Future<void> printWeather() async{

WeatherInfo.getWeather(310000)
.then((value){

WeatherInfo info = value;
print(info);

/* 其他操作 */

});

}
1
2
3
4
5
6
7
8
9
10
11

main(){
printWeather();
}

Future<void> printWeather() async{
WeatherInfo info = await WeatherInfo.getWeather(310000);
print(info);

/* 其他操作 */
}

以上两段代码是等价的。

可以很容易的看出 await 的用法。await 只能用在 async 或者 async* 修饰的方法里面,并且后面跟着的一定要是 Future 对象。

如果是 变量 = await xxx 则等价为 :

1
2
3
xxx.then((value){
变量 = value;
});

同时,await 之后的所有代码,都会放到后面所在 Futurethen 里面。比如以下代码:

1
2
变量 = await xxx();
print(变量);

等价为 :

1
2
3
4
xxx.then((value){
变量 = value;
print(变量);
})

讲到这,可以思考一下,为啥只能在 async 的方法中使用 await
或者说,如果在 普通方法 可以使用 await ,那么会发生什么。

来看一下代码:

1
2
3
4
String getCity(){
String result = await getNetCity();
return result;
}

假设可以在普通方法中使用 await,那么我们可以写出以上代码,利用代码的等价性做一下等价:

1
2
3
4
5
6
7
String getCity(){
getNetCity()
.then((value){
String result = value;
return result;
});
}

好的,那我问你,getCity() 方法的返回值是啥。没有,并且根据 await 后面所有代码都会放到 then 里,所以这个方法永远没有返回值。注意这里没有返回值不是返回空,这样写编译的过程都通过不了。

所以很容易理解为啥 await 只能在 async 方法里使用。

解决回调地狱

接下来看看回调地狱怎么解决,先上原来的代码:

1
2
3
4
5
6
7
8
9
10
main(){
CityInfo.getCity('汕头市')
.then((cityId){

WeatherInfo.getWeather(cityId)
.then((value) => print(value));

});

}

来看看优化版:

1
2
3
4
5
6
7
8
9
main(){
printWeather();
}

Future<void> printWeather() async{
int cityId = await CityInfo.getCity('汕头市');
WeatherInfo info = await WeatherInfo.getWeather(cityId);
print(info);
}

几层回调,我们就用几层 await

这就是实际写项目的时候经常遇到的异步的写法。不得不说,我个人是很喜欢这个语法的,比起 Java 实在是好太多了。

async 和 await 的直观体现

上面说的是在 Future 的角度去理解 asyncawait ,但真正开发中,我们可以不管其中的 Future 的,直接按照宏观体现来写。

首先是 asycn ,以下是一个没有返回值的异步方法:

1
2
3
Future<void> runInAsync() async{
/* 操作 */
}

不用按照 Future 来写,在其他任何地方调用 runInAsunc 方法,这个方法都会异步执行。

如果这个异步方法有返回值,则将上述 void 改为返回值。

比如:

1
2
3
Future<String> runInAsync() async{
/* 操作 */
}

就是一个返回值为 String 的异步方法。

然后是 await ,可以理解为阻塞。

比如:

1
var result = await getImage();

首先,getImage 一定要是一个带返回值的异步方法(这里的带返回值指返回值的 Future 对象泛型不是 void ),然后当我们执行到这一步的时候,遇到 await,当前异步会被阻塞,则可以理解为这个运行过程停住了,开始执行 getImage 的异步方法,等到它执行完毕后,将返回值赋给 result ,然后继续执行下去。

使用 异步 ,阻塞 的模型去理解 asyncawait 说实话能更快的理解,不过其中的具体实现还是应该了解一下的。有时候我们滥用 await 可能会导致渲染 UI 的异步阻塞,导致页面卡顿,这个时候我们就可以放弃 await ,直接在原来的基础上后面传入 then 来在进行一层异步。这些都是要建立在你理解两个关键字和 Future 之间关系的基础上的。包括下一篇要介绍的一些技巧也是如此。

后记

这篇主要是介绍 asyncawait 的通常用法,一般开发知道这一步就够了。当然,在一些特殊情况还是有一些其他技巧的,这些比较不常用的技巧,就留给下一章吧。