Microtask,Future还是postFrameCallback,我应该用哪个?
在这篇文章中,我想带你进入Flutter的深处,了解更多关于调度代码执行的小旅程。作为一个对话的开始,让我们假设我们正在使用标准的BLoC架构,使用Provider库构建一个应用程序。为了使这个任务具有挑战性,在打开一个新的屏幕后,我们将不得不发起一个网络请求,通过互联网获取一些东西。在这种情况下,我们有几个选择来发起我们的请求。
1.在显示我们的屏幕之前获取数据,然后在预先加载数据的情况下显示它,这可能不是最好的选择。这可能不是最好的选择。如果您决定只获取所需的部分数据,您很可能会加载大量不必要的数据或用旋转器阻塞用户界面。
-
在
BLoC
中启动加载过程,就在屏幕显示之前,当创建BLoC
本身或使用协调器对象为您启动它。如果您想保持架构的整洁,这将是推荐的方法。 -
在屏幕的initState中启动加载过程,尝试将这个逻辑封装在屏幕本身。
第三种方案在架构正确性方面可能不是最好的,但实际上是Flutter世界中相当常见的方法。让我们来研究一下,因为它在现实世界的场景中完美地演示了我们的主题。
为了演示的目的,这里是一个示例代码。注意到它有什么问题吗?
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MaterialApp(
title: 'Demo',
home: ChangeNotifierProvider(
create: (_) => MyHomePageBloc(),
child: MyHomePage(),
),
),
);
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
context.read<MyHomePageBloc>().fetchData();
}
@override
Widget build(BuildContext context) {
final bloc = context.watch<MyHomePageBloc>();
return Scaffold(
appBar: AppBar(),
body: Center(
child: bloc.loading ? CircularProgressIndicator() : Text(bloc.data),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<MyHomePageBloc>().fetchData(),
tooltip: 'Fetch',
child: Icon(Icons.add),
),
);
}
}
class MyHomePageBloc with ChangeNotifier {
String data = "Loading";
bool loading = false;
void fetchData() {
loading = true;
data = "Loading";
notifyListeners();
Future.delayed(Duration(seconds: 3), () {
loading = false;
data = "Done";
notifyListeners();
});
}
}
乍一看,似乎一切正常。然而,如果你运行它,它将不可避免地崩溃,你会在日志中看到类似的东西。‘package:flutter/src/widgets/framework.dart’: Failed assertion: line 4349 pos 12: ‘!_dirty’: is not true.
这个错误表明,我们正试图在构建时修改widget树。小组件的 initState 方法是在构建过程的中间调用的,所以从那里修改小组件树的任何尝试都会失败。在我们的例子中,当调用fetch方法时,它会同步执行notifyListeners(),从而导致widget树发生变化。
当你试图做更多看似不相关的事情时,可能会遇到类似的错误。例如,显示一个对话,也会因为类似的原因而失败,因为上下文( Element)当前还没有挂载在小组件树中。
无论你想做什么,你必须延迟代码执行,直到构建过程完成。换句话说,你需要异步执行你的代码。现在说说我们的选择。
如何在Flutter中延迟代码执行?
通过在互联网上研究这个话题,我整理了一个最常见的推荐解决方案列表。你甚至可以找到一些额外的选项,但这里是最引人注目的选项。
- scheduleMicrotask
Future<T>.microtask
Future<T>
Future<T>.delayed
- Timer.run
- WidgetsBinding.addPostFrameCallback
- SchedulerBinding.addPostFrameCallback
你可能会说,这选项还真不少,你说的没错。说到我们上述的问题,任何一种都能解决。不过,既然我们面对如此多的选择,那就让我们放纵一下自己的好奇心,试着了解一下它们之间的区别。
事件循环和多线程
你可能知道,Dart是一个单线程系统。令人惊讶的是,你的应用程序可以同时做多件事情,或者至少看起来是这样。这就是事件循环
的作用。事件循环
从字面上看就是一个执行预定事件的无尽循环(对于iOS开发者来说是Run Loop)。这些事件(或者只是代码块,如果你喜欢的话)必须是轻量级的,否则,你的应用会感觉滞后或完全冻结。每一个事件
,如按钮按下或网络响应,都被安排在事件队列
中,并等待被事件循环
接收和执行。这种设计模式在UI和其他处理任何类型事件的系统中相当常见。这个概念可能很难用两句话解释清楚,所以如果你是新手的话,我建议你在旁边看点东西。不要想得太多,我们从字面上看,我们说的是一个简单的无限循环和一个计划执行的任务列表(代码块),每次循环的迭代都是一个。
我们即将学习的Dart事件循环
聚会的特殊嘉宾是Microtask 。我们的Event Loop
里面有额外的队列,这就是Microtask队列
。关于这个队列,唯一需要注意的是,在事件本身执行之前,所有排定在其中的任务都会在事件循环
的一次迭代中被执行。
遗憾的是,这方面的资料并不多,我看到的最好的解释可以在这里或这里的网络档案中找到。
有了这些知识之后,让我们看看上面列出的所有选项,了解它们的工作方式和它们之间的区别。
事件
任何进入事件队列
的东西。这是你在Flutter中调度异步任务的默认方法。调度一个事件
,我们把它添加到事件队列
中,由事件循环
来接收。这种方法被许多Flutter机制使用,如I/O、手势事件、定时器等。
定时器
Timer是Flutter中异步任务的基础。它用于安排事件队列
中的代码执行,有无延迟。由此产生的有趣事实是,如果队列繁忙,你的定时器将永远不会被执行,即使时间到了。
如何使用。
Timer.run(() {
print("Timer");
});
Future<T>
和Future<T>.delayed
一个众所周知且被广泛使用的Dart功能。这可能会让人感到惊讶,但如果你看一下引擎盖下的东西,你会发现只不过是前述定时器
的一个包装。
如何使用。
Future<void>(() {
print("Future Event");
});
Future<void>.delayed(Duration.zero, () {
print("Future.delayed Event");
});
内部实现(链接)。
factory Future(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
Timer.run(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
微型任务
如前所述,所有预定的微任务
都会在下一个预定事件
之前执行。建议避免使用这个队列,除非绝对需要异步执行代码,但在事件队列
的下一个事件
之前。你也可以把这个队列看作是属于上一个事件的任务队列,因为它们将在下一个事件之前完成。过载这个队列可能会完全冻结你的应用程序,因为它必须先执行这个队列中的所有内容,然后才能进入其事件队列
的下一个迭代,例如处理用户输入甚至渲染应用程序本身。尽管如此,这里有我们的选择。
scheduleMicrotask
顾名思义,在微任务队列
中调度一个块状代码。类似于定时器
,如果出了问题,会让应用程序崩溃。
如何使用。
scheduleMicrotask(() {
print("Microtask");
});
Future<T>.microtask
类似于我们之前看到的,将我们的微任务
包装在一个try-catch
块中,以一种漂亮简洁的方式返回执行结果或错误。
如何使用。
Future<void>.microtask(() {
print("Microtask");
});
内部实现(链接)。
factory Future.microtask(FutureOr<T> computation()) {
_Future<T> result = new _Future<T>();
scheduleMicrotask(() {
try {
result._complete(computation());
} catch (e, s) {
_completeWithErrorCallback(result, e, s);
}
});
return result;
}
后帧回调
之前的两种方法涉及到低级别的事件循环
机制,而我们现在则转向Flutter领域。这个回调在渲染管道完成时被调用,所以它与widget的生命周期相关联。当它被调度时,它只被调用一次,而不是在每个帧上。使用addPostFrameCallback方法,您可以安排一个或多个回调,以便在帧构建完成后执行。所有预定的回调都将在帧的末尾按照添加的顺序被执行。当这个回调被调用的时候,保证了widget构建过程的完成。通过一些烟雾和镜子,你甚至可以访问widget的布局( RenderBox),比如它的大小,以及做其他各种不推荐的黑客。回调本身将在正常的事件队列中运行,Flutter默认使用的几乎是所有的事件。
调度器绑定
这是一个负责绘图回调的 mixin,实现了我们感兴趣的这个方法。
如何使用。
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
WidgetsBinding
我特意加入了这个,因为它经常和SchedulerBinding一起被提及。它从SchedulerBinding
继承了这个方法,并且有额外的方法与我们的主题无关。一般来说,不管你使用SchedulerBinding
还是WidgetsBinding,两者都会执行位于SchedulerBinding中的完全相同的代码。
如何使用。
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
将我们的知识付诸实践
由于今天我们学习了很多理论知识,所以我强烈建议大家先玩一会,确保自己能正确的操作。我们可以在之前的initState中使用下面的代码,并尝试预测它的执行顺序,这看起来并不是一件容易的事情。
SchedulerBinding.instance.addPostFrameCallback((_) {
print("SchedulerBinding");
});
WidgetsBinding.instance.addPostFrameCallback((_) {
print("WidgetsBinding");
});
Timer.run(() {
print("Timer");
});
scheduleMicrotask(() {
print("scheduleMicrotask");
});
Future<void>.microtask(() {
print("Future Microtask");
});
Future<void>(() {
print("Future");
Future<void>.microtask(() {
print("Microtask from Event");
});
});
Future<void>.delayed(Duration.zero, () {
print("Future.delayed");
Future<void>.microtask(() {
print("Microtask from Future.delayed");
});
结束语
现在我们了解了这么多细节,你可以对如何安排你的代码做出一个深思熟虑的决定。作为一个经验法则,如果你需要你的上下文或与Layout或UI相关的东西,使用addPostFrameCallback
。在任何其他情况下,用Future<T>
或Future<T>.delayed
在事件队列中调度应该是足够的。微任务队列
是非常小众的东西,你可能永远不会遇到,但还是值得了解。当然,如果你的任务很重,你就要考虑创建一个Isolate,你可能已经猜到了,这个Isolate将由事件队列
来传达。但这是另一篇文章的主题。谢谢你的时间,我们下次再见。
原文发表于2021年2月7日,https://oleksandrkirichenko.com。
通过www.DeepL.com/Translator(免费版)翻译
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!