区域
异步动态范围
#本文讨论 dart:async 库中与区域相关的 API,重点介绍顶层 runZoned()
和 runZonedGuarded()
函数。在阅读本文之前,请回顾 Future 和错误处理中介绍的技术。
区域使以下任务成为可能
保护您的应用免于因未捕获的异常而退出。例如,一个简单的 HTTP 服务器可能会使用以下异步代码
dartrunZonedGuarded(() { HttpServer.bind('0.0.0.0', port).then((server) { server.listen(staticFiles.serveRequest); }); }, (error, stackTrace) => print('Oh noes! $error $stackTrace'));
在区域中运行 HTTP 服务器使应用能够在服务器的异步代码中出现未捕获(但非致命)错误时继续运行。
将数据(称为区域本地值)与各个区域相关联。
覆盖有限的方法集,例如
print()
和scheduleMicrotask()
,在部分或全部代码中。在代码每次进入或退出区域时执行操作。此类操作可能包括启动或停止计时器,或保存堆栈跟踪。
您可能在其他语言中遇到过类似于区域的东西。Node.js 中的 Domains 是 Dart 区域的灵感来源。Java 的线程局部存储也有一些相似之处。最接近的是 Brian Ford 的 Dart 区域的 JavaScript 端口 zone.js,他在 此视频中描述了它。
区域基础知识
#区域表示调用的异步动态范围。它是作为调用的一部分执行的计算,以及通过该代码注册的异步回调。
例如,在 HTTP 服务器示例中,bind()
、then()
和 then()
的回调都在同一区域中执行,即使用 runZoned()
创建的区域。
在下一个示例中,代码在 3 个不同的区域中执行:区域 #1(根区域)、区域 #2 和 区域 #3。
import 'dart:async'; main() { foo(); var future; runZoned(() { // Starts a new child zone (zone #2). future = new Future(bar).then(baz); }); future.then(qux); } foo() => ...foo-body... // Executed twice (once each in two zones). bar() => ...bar-body... baz(x) => runZoned(() => foo()); // New child zone (zone #3). qux(x) => ...qux-body...
下图显示了代码的执行顺序,以及代码在哪个区域中执行。
每次调用 runZoned()
都会创建一个新区域并在该区域中执行代码。当该代码调度一个任务(例如调用 baz())时,该任务会在调度它的区域中执行。例如,对 qux() 的调用(main() 的最后一行)在 区域 #1(根区域)中运行,即使它附加到一个本身在 区域 #2 中运行的 future。
子区域不会完全替换其父区域。相反,新区域嵌套在其周围的区域内。例如,区域 #2 包含 区域 #3,而 区域 #1(根区域)包含 区域 #2 和 区域 #3。
所有 Dart 代码都在根区域中执行。代码也可能在其他嵌套的子区域中执行,但至少它始终在根区域中运行。
处理未捕获的错误
#区域能够捕获和处理未捕获的错误。
未捕获的错误通常是由于代码使用 throw
引发异常而没有 accompanying catch
语句来处理它而发生的。当 Future 以错误结果完成,但缺少相应的 await
来处理错误时,async
函数中也可能出现未捕获的错误。
未捕获的错误会报告给未能捕获它的当前区域。默认情况下,区域会因未捕获的错误而使程序崩溃。您可以将自己的自定义未捕获错误处理程序安装到新区域,以拦截和处理您喜欢的未捕获的错误。
要引入一个带有未捕获错误处理程序的新区域,请使用 runZoneGuarded
方法。其 onError
回调将成为新区域的未捕获错误处理程序。此回调处理该调用抛出的任何同步错误。
runZonedGuarded(() {
Timer.run(() { throw 'Would normally kill the program'; });
}, (error, stackTrace) {
print('Uncaught error: $error');
});
其他有助于未捕获错误处理的区域 API 包括 Zone.fork
、Zone.runGuarded
和 ZoneSpecification.uncaughtErrorHandler
。
前面的代码有一个异步回调(通过 Timer.run()
)会抛出异常。通常,此异常将是一个未处理的错误并到达顶层(在独立的 Dart 可执行文件中,这将杀死正在运行的进程)。但是,使用分区错误处理程序,错误会传递给错误处理程序,并且不会关闭程序。
try-catch 和分区错误处理程序之间的一个显着区别是,区域在发生未捕获的错误后会继续执行。如果区域内调度了其他异步回调,它们仍然会执行。因此,分区错误处理程序可能会被多次调用。
任何带有未捕获错误处理程序的区域都称为错误区域。错误区域可能会处理源自该区域后代的错误。一个简单的规则确定了错误在 Future 转换序列(使用 then()
或 catchError()
)中处理的位置:Future 链上的错误永远不会跨越错误区域的边界。
如果错误到达错误区域边界,则在该点将其视为未处理的错误。
示例:错误无法进入错误区域
#在以下示例中,第一行引发的错误无法进入错误区域。
var f = new Future.error(499);
f = f.whenComplete(() { print('Outside of zones'); });
runZoned(() {
f = f.whenComplete(() { print('Inside non-error zone'); });
});
runZonedGuarded(() {
f = f.whenComplete(() { print('Inside error zone (not called)'); });
}, (error) { print(error); });
以下是如果您运行该示例所看到的输出
Outside of zones
Inside non-error zone
Uncaught Error: 499
Unhandled exception:
499
...stack trace...
如果您删除对 runZoned()
或 runZonedGuarded()
的调用,则会看到此输出
Outside of zones
Inside non-error zone
Inside error zone (not called)
Uncaught Error: 499
Unhandled exception:
499
...stack trace...
请注意,删除区域或错误区域会导致错误进一步传播。
堆栈跟踪出现是因为错误发生在错误区域之外。如果您在整个代码片段周围添加一个错误区域,则可以避免堆栈跟踪。
示例:错误无法离开错误区域
#如前面的代码所示,错误无法进入错误区域。同样,错误也无法离开错误区域。考虑以下示例
var completer = new Completer();
var future = completer.future.then((x) => x + 1);
var zoneFuture;
runZonedGuarded(() {
zoneFuture = future.then((y) => throw 'Inside zone');
}, (error) { print('Caught: $error'); });
zoneFuture.catchError((e) { print('Never reached'); });
completer.complete(499);
即使 Future 链以 catchError()
结尾,异步错误也无法离开错误区域。在 runZonedGuarded()
中找到的分区错误处理程序会处理该错误。因此,zoneFuture 永远不会完成——既不会有值,也不会有错误。
将区域与 Stream 一起使用
#区域和 Stream 的规则比 Future 更简单
此规则遵循以下准则:Stream 在被侦听之前不应产生副作用。同步代码中的类似情况是 Iterables 的行为,在您请求值之前不会对其进行评估。
示例:将 Stream 与 runZonedGuarded()
一起使用
#以下示例设置一个带有回调的 Stream,然后在 runZonedGuarded()
中在新区域中执行该 Stream
var stream = new File('stream.dart').openRead()
.map((x) => throw 'Callback throws');
runZonedGuarded(() { stream.listen(print); },
(e) { print('Caught error: $e'); });
runZonedGuarded()
中的错误处理程序会捕获回调抛出的错误。以下是输出
Caught error: Callback throws
正如输出所示,回调与监听区域相关联,而不是与调用 map()
的区域相关联。
存储区域本地值
#如果您曾经想使用静态变量,但由于多个并发运行的计算相互干扰而无法使用,请考虑使用区域本地值。您可能需要添加一个区域本地值来帮助进行调试。另一个用例是处理 HTTP 请求:您可以在区域本地值中包含用户 ID 及其授权令牌。
使用 runZoned()
的 zoneValues
参数将值存储在新创建的区域中。
runZoned(() {
print(Zone.current[#key]);
}, zoneValues: { #key: 499 });
要读取区域本地值,请使用区域的索引运算符和值的键:[key]
。任何对象都可以用作键,只要它具有兼容的 operator ==
和 hashCode
实现即可。通常,键是符号字面量:#identifier
。
您无法更改键映射到的对象,但可以操作该对象。例如,以下代码将一个项添加到区域本地列表中。
runZoned(() {
Zone.current[#key].add(499);
print(Zone.current[#key]); // [499]
}, zoneValues: { #key: [] });
区域从其父区域继承区域本地值,因此添加嵌套区域不会意外删除现有值。但是,嵌套区域可以覆盖父值。
示例:使用区域本地值进行调试日志
#假设您有两个文件,foo.txt 和 bar.txt,并且想要打印它们的所有行。该程序可能如下所示
import 'dart:async';
import 'dart:convert';
import 'dart:io';
Future splitLinesStream(stream) {
return stream
.transform(ASCII.decoder)
.transform(const LineSplitter())
.toList();
}
Future splitLines(filename) {
return splitLinesStream(new File(filename).openRead());
}
main() {
Future.forEach(['foo.txt', 'bar.txt'],
(file) => splitLines(file)
.then((lines) { lines.forEach(print); }));
}
这个程序可以工作,但假设您现在想知道每行来自哪个文件,并且您不能只是将文件名参数添加到 splitLinesStream()
中。使用区域本地值,您可以将文件名添加到返回的字符串中(新行已突出显示)
import 'dart:async';
import 'dart:convert';
import 'dart:io';
Future splitLinesStream(stream) {
return stream
.transform(ASCII.decoder)
.transform(const LineSplitter())
.map((line) => '${Zone.current[#filename]}: $line')
.toList();
}
Future splitLines(filename) {
return runZoned(() {
return splitLinesStream(new File(filename).openRead());
}, zoneValues: { #filename: filename });
}
main() {
Future.forEach(['foo.txt', 'bar.txt'],
(file) => splitLines(file)
.then((lines) { lines.forEach(print); }));
}
请注意,新代码不会修改函数签名,也不会将文件名从 splitLines()
传递到 splitLinesStream()
。相反,它使用区域本地值来实现类似于静态变量的功能,该功能在异步上下文中有效。
覆盖功能
#使用 runZoned()
的 zoneSpecification
参数来覆盖由区域管理的功能。该参数的值是一个 ZoneSpecification 对象,您可以使用它来覆盖以下任何功能
- 派生子区域
- 在区域中注册和运行回调
- 调度微任务和定时器
- 处理未捕获的异步错误(
runZonedGuarded()
是此操作的快捷方式) - 打印
示例:覆盖 print
#作为覆盖功能的一个简单示例,以下是一种静音区域内所有打印的方法
import 'dart:async';
main() {
runZoned(() {
print('Will be ignored');
}, zoneSpecification: new ZoneSpecification(
print: (self, parent, zone, message) {
// Ignore message.
}));
}
在派生的区域内,print()
函数被指定的打印拦截器覆盖,该拦截器只是丢弃消息。覆盖打印是可能的,因为 print()
(例如 scheduleMicrotask()
和 Timer 构造函数)使用当前区域 (Zone.current
) 来完成其工作。
拦截器和委托的参数
#正如打印示例所示,拦截器将三个参数添加到 Zone 类的相应方法中定义的参数中。例如,Zone 的 print()
方法有一个参数:print(String line)
。由 ZoneSpecification 定义的 print()
的拦截器版本有四个参数:print(Zone self, ZoneDelegate parent, Zone zone, String line)
。
三个拦截器参数始终以相同的顺序出现,在任何其他参数之前。
self
- 正在处理回调的区域。
parent
- 一个代表父区域的 ZoneDelegate。使用它将操作转发到父区域。
zone
- 操作起源的区域。某些操作需要知道操作是在哪个区域上调用的。例如,
zone.fork(specification)
必须创建一个新区域作为zone
的子区域。作为另一个示例,即使您将scheduleMicrotask()
委托给另一个区域,原始zone
也必须是执行微任务的区域。
当拦截器将方法委托给父区域时,该方法的父区域 (ZoneDelegate) 版本只有一个额外的参数:zone
,即原始调用起源的区域。例如,ZoneDelegate 上 print()
方法的签名是 print(Zone zone, String line)
。
以下是另一个可拦截方法的参数示例,scheduleMicrotask()
| 定义位置 | 方法签名 | | Zone | void scheduleMicrotask(void f())
| | ZoneSpecification | void scheduleMicrotask(Zone self, ZoneDelegate parent, Zone zone, void f())
| | ZoneDelegate | void scheduleMicrotask(Zone zone, void f())
|
示例:委托给父区域
#这是一个示例,展示了如何委托给父区域
import 'dart:async';
main() {
runZoned(() {
var currentZone = Zone.current;
scheduleMicrotask(() {
print(identical(currentZone, Zone.current)); // prints true.
});
}, zoneSpecification: new ZoneSpecification(
scheduleMicrotask: (self, parent, zone, task) {
print('scheduleMicrotask has been called inside the zone');
// The origin `zone` needs to be passed to the parent so that
// the task can be executed in it.
parent.scheduleMicrotask(zone, task);
}));
}
示例:在进入和离开区域时执行代码
#假设您想知道某些异步代码花费了多少时间执行。您可以通过将代码放入区域中,每次进入该区域时启动计时器,并在离开该区域时停止计时器来实现此目的。
向 ZoneSpecification 提供 run*
参数可以指定区域执行的代码。
run*
参数—run
、runUnary
和 runBinary
—指定每次要求区域执行代码时要执行的代码。这些参数分别适用于零参数、一参数和两参数回调。run
参数也适用于在调用 runZoned()
之后立即执行的初始同步代码。
这是一个使用 run*
分析代码的示例
final total = new Stopwatch();
final user = new Stopwatch();
final specification = new ZoneSpecification(
run: (self, parent, zone, f) {
user.start();
try { return parent.run(zone, f); } finally { user.stop(); }
},
runUnary: (self, parent, zone, f, arg) {
user.start();
try { return parent.runUnary(zone, f, arg); } finally { user.stop(); }
},
runBinary: (self, parent, zone, f, arg1, arg2) {
user.start();
try {
return parent.runBinary(zone, f, arg1, arg2);
} finally {
user.stop();
}
});
runZoned(() {
total.start();
// ... Code that runs synchronously...
// ... Then code that runs asynchronously ...
.then((...) {
print(total.elapsedMilliseconds);
print(user.elapsedMilliseconds);
});
}, zoneSpecification: specification);
在此代码中,每个 run*
重写只是启动用户计时器,执行指定的函数,然后停止用户计时器。
示例:处理回调
#向 ZoneSpecification 提供 register*Callback
参数来包装或更改回调代码—在区域中异步执行的代码。与 run*
参数一样,register*Callback
参数有三种形式:registerCallback
(用于没有参数的回调)、registerUnaryCallback
(一个参数)和 registerBinaryCallback
(两个参数)。
这是一个示例,使区域在代码消失到异步上下文之前保存堆栈跟踪。
import 'dart:async';
get currentStackTrace {
try {
throw 0;
} catch(_, st) {
return st;
}
}
var lastStackTrace = null;
bar() => throw "in bar";
foo() => new Future(bar);
main() {
final specification = new ZoneSpecification(
registerCallback: (self, parent, zone, f) {
var stackTrace = currentStackTrace;
return parent.registerCallback(zone, () {
lastStackTrace = stackTrace;
return f();
});
},
registerUnaryCallback: (self, parent, zone, f) {
var stackTrace = currentStackTrace;
return parent.registerUnaryCallback(zone, (arg) {
lastStackTrace = stackTrace;
return f(arg);
});
},
registerBinaryCallback: (self, parent, zone, f) {
var stackTrace = currentStackTrace;
return parent.registerBinaryCallback(zone, (arg1, arg2) {
lastStackTrace = stackTrace;
return f(arg1, arg2);
});
},
handleUncaughtError: (self, parent, zone, error, stackTrace) {
if (lastStackTrace != null) print("last stack: $lastStackTrace");
return parent.handleUncaughtError(zone, error, stackTrace);
});
runZoned(() {
foo();
}, zoneSpecification: specification);
}
继续运行该示例。您将看到一个“last stack”跟踪 (lastStackTrace
),其中包含 foo()
,因为 foo()
是同步调用的。下一个堆栈跟踪 (stackTrace
) 来自异步上下文,该上下文知道 bar()
但不知道 foo()
。
实现异步回调
#即使您正在实现异步 API,您也可能不必处理区域。例如,尽管您可能希望 dart:io 库跟踪当前区域,但它依赖于 dart:async 类(例如 Future 和 Stream)的区域处理。
如果您确实显式处理区域,则需要注册所有异步回调,并确保在注册回调的区域中调用每个回调。Zone 的 bind*Callback
帮助方法使此任务更加容易。它们是 register*Callback
和 run*
的快捷方式,确保每个回调都在该区域中注册和运行。
如果您需要比 bind*Callback
提供的更多控制,则需要使用 register*Callback
和 run*
。您可能还需要使用 Zone 的 run*Guarded
方法,该方法将调用包装到 try-catch 中,并在发生错误时调用 uncaughtErrorHandler
。
总结
#区域非常适合保护您的代码免受异步代码中未捕获的异常的影响,但它们可以做更多的事情。您可以将数据与区域关联,并且可以覆盖核心功能,例如打印和任务调度。区域可以更好地进行调试,并提供可用于诸如分析等功能的挂钩。
更多资源
#- 与区域相关的 API 文档
- 阅读 runZoned()、runZonedGuarded()、Zone、ZoneDelegate 和 ZoneSpecification 的文档。
- stack_trace
- 使用 stack_trace 库的 Chain 类,您可以获得更好地执行异步代码的堆栈跟踪。有关详细信息,请参阅 pub.dev 站点上的 stack_trace 包。
更多示例
#以下是一些更复杂的区域使用示例。
- task_interceptor 示例
- task_interceptor.dart 中的玩具区域拦截
scheduleMicrotask
、createTimer
和createPeriodicTimer
以模拟 Dart 原语的行为,而不会让步于事件循环。 - stack_trace 包的源代码
- stack_trace 包使用区域来形成堆栈跟踪链,以用于调试异步代码。使用的区域功能包括错误处理、区域本地值和回调。您可以在 stack_trace GitHub 项目中找到 stack_trace 源代码。
- dart:html 和 dart:async 的源代码
- 这两个 SDK 库实现了具有异步回调的 API,因此它们处理区域。您可以在 Dart GitHub 项目的 sdk/lib 目录下浏览或下载它们的源代码。
感谢 Anders Johnsen 和 Lasse Reichstein Nielsen 对本文的审阅。
除非另有说明,否则本网站上的文档反映了 Dart 3.6.0。页面最后更新时间为 2014-03-03。 查看源代码 或 报告问题。