dart笔记之异步编程(一)Dart语言异步编程基础
1. 异步编程简介
1.1 同步编程 与 异步编程 的区别
异步编程是一种处理可能需要一些时间完成的任务,如IO操作(文件读写,数据库访问,网络请求等)的方式。在异步编程中,主程序继续执行其余的任务,而不必等待该操作完成,从而提高应用程序的效率和响应性。
1.1 异步编程与同步编程的比较
在同步编程中,代码是自上而下按顺序执行的,每个操作必须在执行下一个操作之前完成。如果某个操作需要花费长时间,则应用程序将被阻塞,直到该操作完成。
相反,在异步编程中,如果执行像IO操作那样可能需要一些时间来完成的操作,代码继续运行,不需要等待该操作完成。然后,当IO操作完成时,通过回调、Promise/Future 或者 async/await等机制,通知主程序处理结果。
1.1.2 Dart 中的异步编程
在Dart中,Future和await是实现异步编程的主要工具。让我们来看一下他们如何工作。
同步代码示例:
void main() { print('Fetching user order...'); fetchUserOrder(); print('Finished.'); } void fetchUserOrder() { // 模拟延迟 for (var i = 0; i < 10000000000; i++); print('Chicken sandwich'); }
以上代码中,fetchUserOrder()函数需要一些时间去完成(通过模拟一个长的for循环实现延迟)。在此期间,程序必须等待fetchUserOrder()完成才能执行其他任务。
对于简单的代码来说,同步(或者“阻塞”)实现可能没什么问题。
然而,如果你的应用程序需要执行许多延时长或必须等待的操作,如网络请求或数据库访问,那么同步代码将导致应用程序冻结,用户界面无法响应。
例如:
void main() { print('Fetching user order...'); fetchUserOrder(); print('Finished.'); } // 使用了async关键字,所以fetchUserOrder()现在是一个"异步"函数 Future<void> fetchUserOrder() async { // 模拟网络请求 await Future.delayed(Duration(seconds: 2)); print('Chicken sandwich'); }
在这个示例中,我们使用Future.delayed() 函数模拟网络请求,该函数在给定的延迟后让其返回一个完成的Future。我们通过在函数签名中添加async关键字,并在Future.delayed()调用前添加await关键字,将fetchUserOrder()转换为异步函数,主应用程序不再需要一直等待fetchUserOrder()函数的完成。
结果,主应用程序继续执行,Finished将打印出来,fetchUserOrder()继续运行,在两秒后完成,并打印Chicken sandwich。
1.2 异步编程在程序中的应用的优点
一个优秀的移动应用应该能够在处理复杂任务或者等待操作完成时,仍能保持与用户的交互和响应,这正是异步编程的优势所在。在开发移动应用程序时,当我们需要执行可能会花费一定时间的操作,如读取大文件、访问网络资源、处理大量数据等,异步编程都是必然的选择。与此同时,异步编程使得我们的应用程序可以保持响应状态,这在用户界面(UI)中至关重要。
在移动应用程序中,UI更新通常在所谓的“主线程”或“UI线程”上执行。如果这个线程被阻塞(即,它正在等待同步操作完成),应用的UI就会冻结,从而导致糟糕的用户体验。
例如,当在主线程上执行网络请求时(这本身可能需要几秒钟),如果应用程序的UI在这段期间停止响应,用户可能认为应用程序已经崩溃或者挂起,可能会直接退出应用。
通过异步编程,我们可以将这些费时的操作放在一个单独的工作线程上进行,而主线程可以继续处理其他任务,如处理用户输入或更新UI。当耗时操作完成后,结果可以被发送回主线程用于更新UI或执行其他后续操作。
2. Dart 的并发模型:事件循环和微任务队列
2.1 Dart语言中的事件循环
要理解 Dart 的异步编程,您需要先了解 Dart 的并发模型和如何管理任务的执行。在 Dart 中,事件循环是实现并发的关键元素。
2.1.1 事件循环简介
在Dart中,所有的代码(无论是同步还是异步的)都在事件循环中运行。事件循环可以看作是一个等待处理任务的无限循环(或队列)。这些任务可以是用户交互,例如点击或键盘事件,或者I/O事件,例如文件读写和网络请求等。
2.1.2 事件循环工作原理
- 获取任务:事件循环等待接收任务。
- 执行任务:一旦接收到任务,事件循环会执行该任务。
- 任务完成:一旦任务完成,它会被从队列中移除。
- 继续循环:事件循环检查是否还有任务需要执行。如果有,它会重复第二步和第三步。如果没有,它将继续等待(第一步)。
值得注意的是,事件循环一次只执行一个事件。
2.1.3 事件队列与微任务队列
在Dart中,事件循环管理两个任务队列:事件队列和微任务队列。
- 事件队列:包含I/O,定时和动画帧等任务。一些典型的事件是:文件已经被读取完成、一个新的WebSocket消息已经到达、屏幕需要重新绘制等等。
- 微任务队列:微任务队列通常包含任务我们希望它们立即运行,但又希望让它们在当前任务完成之后再运行。例如,我们可能希望在当前任务完成后(在控制流回到事件循环之前)在命令行上打印一条消息。
微任务的概念是从JavaScript中借用过来的,本质上它是一种让你控制代码在事件循环的顺序的机制。微任务队列中的任务会在事件队列中的任务之前运行。
2.1.4 使用Future创建微任务
在Dart中,我们可以使用 Future
类在微任务队列中添加任务。以下是一个创建微任务的例子:
Future(() { print('Running in the microtask queue.'); });
2.1.5 小结
事件循环是Dart并发模型的核心,它管理了两个队列:事件队列和微任务队列。理解它们的工作原理以及如何使用它们可以帮助我们更好地编写异步代码。在接下来的章节中,我们将详细介绍 Future
和 Stream
类,并通过一些示例展示如何在事件循环中使用它们。
2.2 使用微任务队列
在了解了 Dart 的事件循环和两种任务队列之后,接下来我们将重点讨论“微任务队列”的使用方式。在这个过程中,您将学习如何使用 Future 来创建和管理微任务。
2.2.1 创建微任务
在 Dart 中,微任务是通过 Future 对象来创建和添加到微任务队列的。以下是一个创建微任务的基本示例:
Future(() { print('Running in the microtask queue.'); });
在上述代码示例中,我们使用 Future
构造函数创建了一个新的 Future,并提供了一个函数作为参数。此函数就是我们希望作为微任务执行的代码。
2.2.2 Future 的状态
每一个 Future 对象有三种状态:
- Uncompleted:Future 任务已经创建,但还未执行完。
- Completed with a value:Future 任务已经成功完成,并返回了一个值。
- Completed with an error:Future 任务已经完成,但在执行过程中发生了错误。
通过调用 Future 对象的 then
方法,我们可以注册回调函数,当 Future 完成时(无论成功或失败),这个回调函数会被调用。并且,在注册回调函数时,无论 Future 的状态是什么,回调函数都只会在微任务队列中执行。
2.2.3 处理错误
您可以使用 catchError
方法来处理 Future 可能抛出的错误。在以下的代码示例中,我们注册了一个回调函数来处理任何可能向上抛出的错误:
Future(() { throw 'Error!'; }).catchError((error) { print('Caught error: $error'); });
2.2.4 Future API
Future 类有很多其他有用的方法:
Future.value
:立即返回一个完成的 Future,其值为给定的参数。Future.delayed
:在给定的延迟时间之后,返回一个完成的 Future,其值为未完成的 Future 返回的结果。Future.wait
:等待所有给定的 Future 完成。
2.2.5 小结
微任务队列是一种有效管理代码执行顺序的机制。在 Dart 中,我们可以使用 Future
类来创建和管理微任务队列中的任务。理解和掌握 Future
的使用将有助于我们编写更为强大和健壮的异步代码。
2.3 使用微任务队列
在理解了事件循环的工作机制以及如何使用微任务队列之后,下面来看我们如何可以将二者结合起来在你的异步编程中使用。
2.3.1 事件循环和微任务队列交互
事件循环和微任务队列的交互过程是:每当事件循环没有任务可以执行时,它就会检查微任务队列中是否有未处理的任务。如果有,那么它将处理靠前的一个微任务。一旦微任务队列为空或者正在处理的微任务排队了一个事件,那么事件循环就会转向事件队列。这个过程在事件循环终止之前将一直持续。以下代码示例说明了这个交互的过程:
Future(() { print('Running in the microtask queue.'); }); print('Running in the event loop.');
在上面的代码中,当事件循环运行完 print('Running in the event loop.');
后,它将检查微任务队列并运行 print('Running in the microtask queue.');
。
2.3.2 统一的异步模型
由于微任务始终在事件处理前执行,因此 Dart 的并发模型保证了在两个事件处理之间可以安全地运行微任务而不会被中断。这对于编写需要维护一致状态的代码非常重要。
例如,如果你在执行任务的时候触发了一个事件,可以并且应该将此任务的剩余部分排队到一个微任务中,以确保在处理下一个事件之前完成任务。这就像是一种更细粒度的事件分割,可以保证在处理事件时不会被其他事件打断,同时允许在处理完一部分事件后插入更多的微任务来处理。
2.3.3 小结
事件循环 和 微任务队列 的交互为异步编程提供了一种强大而灵活的工具。理解这两者的交互和使用能够帮助我们更好的编写和理解 Dart 中的异步代码,从而创建出更强健和高效的应用。
3. Dart 与 JavaScript 异步模型的对比
3.1 JavaScript 中的事件循环和任务队列
JavaScript 的异步处理和 Dart 有很多相似之处。它有一个事件循环(Event Loop),并且事件循环也依赖任务队列(Task Queue)将任务按照类型区分和处理。但与 Dart 不同,JavaScript 有两种任务队列:宏任务队列(MacroTask Queue)和微任务队列(MicroTask Queue)。这两者的细节如下:
- 宏任务队列:包括了大部分的异步任务类型,例如
setTimeout
、setInterval
、Ajax
等。 - 微任务队列:包括了
Promise
等高优先级的任务。
在 JavaScript 中,事件循环先检查宏任务队列,然后当宏任务队列为空时,会执行微任务队列中的全部任务。而且,在执行每一波宏任务之间,JavaScript 会清空整个微任务队列。
3.2 Dart 与 JavaScript 的任务队列比较
- 在 JavaScript 中,使用
Promise
创建的任务被添加到微任务队列中,而在 Dart 中,需要使用Future
创建任务来添加到微任务队列。 - JavaScript 的事件循环先执行宏任务,然后在每个宏任务执行结束之后,会处理整个微任务队列。而在 Dart 中,事件循环将检查并按序运行微任务队列中的任务,然后再检查并执行事件队列中的任务。
在 JavaScript 中,我们可以使用 setTimeout(一种宏任务)和 Promise(一种微任务)来演示两种不同的任务队列:
console.log('Start'); setTimeout(() => { console.log('setTimeout'); }, 0); Promise.resolve().then(() => { console.log('Promise'); }); console.log('End');
这段 JavaScript代码的输出顺序将会是:‘Start’ -> ‘End’ -> ‘Promise’ -> ‘setTimeout’。
这是因为在 JavaScript 中,微任务队列会在每个宏任务之后全部执行。
在 Dart 中,我们可以使用 Future 来演示微任务队列的工作方式:
print('Start'); Future(() => print('Future')); print('End');
这段代码输出顺序是:‘Start’ -> ‘End’ -> ‘Future’。
这是因为 Dart 的事件循环会在没有任务可执行时检查微任务队列,处理微任务,然后转向事件队列。
通过以上的示例,在 JavaScript 中,一旦一个宏任务入队并执行,事件循环将在每次宏任务之后执行完微任务队列中的所有任务。而在 Dart 中,事件循环会在处理每个任务之后检查并执行微任务队列中的任务,然后转向事件队列。这就是 Dart 和 JavaScript 在处理异步任务上的主要区别。
3.3 对比和总结
- Dart 和 JavaScript 都提供了灵活的异步编程模型。但是,Dart 为了确保事件不被中断,并且在两个事件处理之间供任务处理,会优先执行微任务队列中所有的任务。这一点上,Dart 提供了一种更细粒度的异步操作方式。
- JavaScript 的异步模型也很强大,但若微任务队列中有许多高优先级任务需要处理,可能会导致一定程度的阻塞。不过,这取决于实际用例和开发人员如何管理异步任务。
虽然 Dart 和 JavaScript 在异步模型上有相似之处,但它们在微任务队列的使用和处理上有所不同。理解这些差异可以帮助我们更好地利用两种语言的特性,编写出更强健、高效的异步代码