區域
非同步動態範圍
#本文討論 dart:async 函式庫中與 Zone 相關的 API,重點在於最上層的 runZoned()
和 runZonedGuarded()
函式。在閱讀本文之前,請先複習 Futures 和錯誤處理中涵蓋的技術。
Zone 讓以下任務成為可能
保護您的應用程式免於因未捕獲的例外狀況而結束。例如,一個簡單的 HTTP 伺服器可能會使用以下非同步程式碼
dartrunZonedGuarded(() { HttpServer.bind('0.0.0.0', port).then((server) { server.listen(staticFiles.serveRequest); }); }, (error, stackTrace) => print('Oh noes! $error $stackTrace'));
在 Zone 中執行 HTTP 伺服器可讓應用程式在伺服器的非同步程式碼中發生未捕獲(但非致命性)錯誤時,仍能繼續執行。
將資料(稱為 Zone 本機值)與個別 Zone 建立關聯。
在部分或全部程式碼中,覆寫一組有限的方法,例如
print()
和scheduleMicrotask()
。在程式碼每次進入或離開 Zone 時執行操作。此類操作可能包括啟動或停止計時器,或儲存堆疊追蹤。
您可能在其他語言中遇到過類似 Zone 的概念。Node.js 中的 Domains 是 Dart Zone 的靈感來源。Java 的 thread-local storage 也有些相似之處。最接近的是 Brian Ford 的 Dart Zone JavaScript 移植版本 zone.js,他在這部影片中描述了它。
Zone 基礎知識
#Zone 代表呼叫的非同步動態範圍。它是作為呼叫的一部分執行的計算,以及由該程式碼註冊的非同步回呼(遞移地)。
例如,在 HTTP 伺服器範例中,bind()
、then()
和 then()
的回呼都在同一個 Zone 中執行,也就是使用 runZoned()
建立的 Zone。
在下一個範例中,程式碼在 3 個不同的 Zone 中執行:Zone #1(根 Zone)、Zone #2 和 Zone #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...
下圖顯示程式碼的執行順序,以及程式碼在哪個 Zone 中執行。
每次呼叫 runZoned()
都會建立一個新的 Zone,並在該 Zone 中執行程式碼。當該程式碼排程任務(例如呼叫 baz())時,該任務會在排程它的 Zone 中執行。例如,即使 qux() 的呼叫(main() 的最後一行)附加到本身在 Zone #2 中執行的 future,它仍在 Zone #1(根 Zone)中執行。
子 Zone 不會完全取代其父 Zone。相反地,新的 Zone 會巢狀於其周圍的 Zone 內。例如,Zone #2 包含 Zone #3,而 Zone #1(根 Zone)同時包含 Zone #2 和 Zone #3。
所有 Dart 程式碼都在根 Zone 中執行。程式碼也可能在其他巢狀子 Zone 中執行,但至少始終在根 Zone 中執行。
處理未捕獲的錯誤
#Zone 能夠捕獲和處理未捕獲的錯誤。
未捕獲的錯誤通常是因為程式碼使用 throw
引發例外狀況,但沒有隨附的 catch
陳述式來處理它而發生。當 Future 完成並產生錯誤結果,但缺少對應的 await
來處理錯誤時,非同步函式中也可能出現未捕獲的錯誤。
未捕獲的錯誤會回報給未能捕獲它的目前 Zone。預設情況下,Zone 會在回應未捕獲的錯誤時使程式崩潰。您可以將自己的自訂未捕獲錯誤處理常式安裝到新的 Zone,以攔截和處理未捕獲的錯誤,無論您喜歡哪種方式。
若要引入具有未捕獲錯誤處理常式的新 Zone,請使用 runZoneGuarded
方法。其 onError
回呼會成為新 Zone 的未捕獲錯誤處理常式。此回呼會處理呼叫擲回的任何同步錯誤。
runZonedGuarded(() {
Timer.run(() { throw 'Would normally kill the program'; });
}, (error, stackTrace) {
print('Uncaught error: $error');
});
其他有助於未捕獲錯誤處理的 Zone API 包括 Zone.fork
、Zone.runGuarded
和 ZoneSpecification.uncaughtErrorHandler
。
前面的程式碼具有一個非同步回呼 (透過 Timer.run()
),它會擲回例外狀況。正常情況下,此例外狀況會是一個未處理的錯誤並到達最上層(在獨立 Dart 可執行檔中,這會終止正在執行的程序)。但是,透過 Zone 化的錯誤處理常式,錯誤會傳遞給錯誤處理常式,而不會關閉程式。
try-catch 和 Zone 化的錯誤處理常式之間一個值得注意的差異是,Zone 在發生未捕獲的錯誤後會繼續執行。如果在 Zone 內排程了其他非同步回呼,它們仍然會執行。因此,Zone 化的錯誤處理常式可能會被多次調用。
任何具有未捕獲錯誤處理常式的 Zone 都稱為錯誤 Zone。錯誤 Zone 可能會處理源自該 Zone 後代的錯誤。一個簡單的規則決定了在 future 轉換序列(使用 then()
或 catchError()
)中錯誤的處理位置:Future 鏈上的錯誤永遠不會跨越錯誤 Zone 的邊界。
如果錯誤到達錯誤 Zone 邊界,則在該點會將其視為未處理的錯誤。
範例:錯誤無法跨越到錯誤 Zone
#在以下範例中,第一行引發的錯誤無法跨越到錯誤 Zone。
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...
請注意移除 Zone 或錯誤 Zone 如何導致錯誤進一步傳播。
堆疊追蹤出現是因為錯誤發生在錯誤 Zone 之外。如果您在整個程式碼片段周圍新增一個錯誤 Zone,則可以避免堆疊追蹤。
範例:錯誤無法離開錯誤 Zone
#如前面的程式碼所示,錯誤無法跨越到錯誤 Zone。同樣地,錯誤也無法跨越出錯誤 Zone。請考慮以下範例
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()
結尾,非同步錯誤也無法離開錯誤 Zone。在 runZonedGuarded()
中找到的 Zone 化的錯誤處理常式處理了錯誤。因此,zoneFuture 永遠不會完成,既不會產生值,也不會產生錯誤。
將 Zone 與 Streams 一起使用
#Zone 和 stream 的規則比 future 的規則更簡單
此規則遵循 stream 在被監聽之前不應有副作用的準則。同步程式碼中的類似情況是 Iterables 的行為,它們在您要求值之前不會被評估。
範例:將 stream 與 runZonedGuarded()
一起使用
#以下範例設定一個具有回呼的 stream,然後使用 runZonedGuarded()
在新的 Zone 中執行該 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
如輸出所示,回呼與監聽 Zone 相關聯,而不是與呼叫 map()
的 Zone 相關聯。
儲存 Zone 本機值
#如果您曾經想使用靜態變數,但因為多個並行執行的計算互相干擾而無法使用,請考慮使用 Zone 本機值。您可以新增 Zone 本機值以協助偵錯。另一個用例是處理 HTTP 請求:您可以將使用者 ID 及其授權權杖放在 Zone 本機值中。
使用 runZoned()
的 zoneValues
引數將值儲存在新建立的 Zone 中
runZoned(() {
print(Zone.current[#key]);
}, zoneValues: { #key: 499 });
若要讀取 Zone 本機值,請使用 Zone 的索引運算子和值的鍵:[key]
。任何物件都可以用作鍵,只要它具有相容的 operator ==
和 hashCode
實作即可。通常,鍵是符號字面值:#identifier
。
您無法變更鍵對應到的物件,但您可以操作該物件。例如,以下程式碼將項目新增至 Zone 本機列表
runZoned(() {
Zone.current[#key].add(499);
print(Zone.current[#key]); // [499]
}, zoneValues: { #key: [] });
Zone 從其父 Zone 繼承 Zone 本機值,因此新增巢狀 Zone 不會意外捨棄現有值。但是,巢狀 Zone 可以遮蔽父值。
範例:將 Zone 本機值用於偵錯記錄
#假設您有兩個檔案 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()
。透過 Zone 本機值,您可以將檔案名稱新增至傳回的字串 (新行已醒目提示)
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()
。相反地,它使用 Zone 本機值來實作類似於靜態變數的功能,該功能在非同步環境中運作。
覆寫功能
#使用 runZoned()
的 zoneSpecification
引數來覆寫由 Zone 管理的功能。引數的值是一個 ZoneSpecification 物件,您可以使用它來覆寫以下任何功能
- 分支子 Zone
- 在 Zone 中註冊和執行回呼
- 排程微任務和計時器
- 處理未捕獲的非同步錯誤 (
runZonedGuarded()
是此功能的捷徑) - 列印
範例:覆寫 print
#作為覆寫功能的一個簡單範例,以下是一種靜音 Zone 內所有列印的方法
import 'dart:async';
main() {
runZoned(() {
print('Will be ignored');
}, zoneSpecification: new ZoneSpecification(
print: (self, parent, zone, message) {
// Ignore message.
}));
}
在分支的 Zone 內,print()
函式會被指定的列印攔截器覆寫,該攔截器只會捨棄訊息。覆寫列印是可能的,因為 print()
(像 scheduleMicrotask()
和 Timer 建構子一樣) 使用目前的 Zone (Zone.current
) 來執行其工作。
攔截器和委派的引數
#如列印範例所示,攔截器會將三個引數新增至 Zone 類別對應方法中定義的引數。例如,Zone 的 print()
方法有一個引數:print(String line)
。ZoneSpecification 定義的 print()
攔截器版本有四個引數:print(Zone self, ZoneDelegate parent, Zone zone, String line)
。
這三個攔截器引數始終以相同的順序出現,在任何其他引數之前。
self
- 處理回呼的 Zone。
parent
- 代表父 Zone 的 ZoneDelegate。使用它將操作轉發到父 Zone。
zone
- 操作起源的 Zone。某些操作需要知道在哪個 Zone 上調用了該操作。例如,
zone.fork(specification)
必須建立一個新的 Zone 作為 zone 的子項。另一個範例是,即使您將scheduleMicrotask()
委派給另一個 Zone,原始 Zone 也必須是執行微任務的 Zone。
當攔截器將方法委派給父 Zone 時,父 Zone (ZoneDelegate) 版本的方法只有一個額外的引數:zone
,即原始呼叫起源的 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())
|
範例:委派給父 Zone
#以下範例示範如何委派給父 Zone
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);
}));
}
範例:在進入和離開 Zone 時執行程式碼
#假設您想知道某些非同步程式碼花費了多少時間執行。您可以透過將程式碼放在 Zone 中,每次進入 Zone 時啟動計時器,並在每次離開 Zone 時停止計時器來完成此操作。
將 run*
參數提供給 ZoneSpecification 可讓您指定 Zone 執行的程式碼。
run*
參數(run
、runUnary
和 runBinary
)指定每次要求 Zone 執行程式碼時要執行的程式碼。這些參數分別適用於零引數、單一引數和雙引數回呼。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*
覆寫都只會啟動使用者計時器、執行指定的函式,然後停止使用者計時器。
範例:處理回呼
#將 register*Callback
參數提供給 ZoneSpecification 以包裝或變更回呼程式碼,即在 Zone 中非同步執行的程式碼。與 run*
參數一樣,register*Callback
參數具有三種形式:registerCallback
(適用於沒有引數的回呼)、registerUnaryCallback
(單一引數) 和 registerBinaryCallback
(雙引數)。
以下範例使 Zone 在程式碼消失到非同步環境之前儲存堆疊追蹤。
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);
}
繼續執行範例。您會看到一個「最後堆疊」追蹤 (lastStackTrace
),其中包含 foo()
,因為 foo()
是同步呼叫的。下一個堆疊追蹤 (stackTrace
) 來自非同步環境,它知道 bar()
但不知道 foo()
。
實作非同步回呼
#即使您正在實作非同步 API,您也可能不必處理 Zone。例如,雖然您可能期望 dart:io 函式庫追蹤目前的 Zone,但它實際上依賴於 dart:async 類別 (例如 Future 和 Stream) 的 Zone 處理。
如果您確實明確處理 Zone,則需要註冊所有非同步回呼,並確保每個回呼都在註冊它的 Zone 中調用。Zone 的 bind*Callback
輔助方法使此任務更容易。它們是 register*Callback
和 run*
的捷徑,確保每個回呼都在該 Zone 中註冊和執行。
如果您需要比 bind*Callback
提供的更多控制,則需要使用 register*Callback
和 run*
。您可能也想使用 Zone 的 run*Guarded
方法,它會將呼叫包裝在 try-catch 中,並在發生錯誤時調用 uncaughtErrorHandler
。
摘要
#Zone 非常適合保護您的程式碼免於非同步程式碼中未捕獲的例外狀況,但它們可以做更多事情。您可以將資料與 Zone 建立關聯,並且可以覆寫核心功能,例如列印和任務排程。Zone 可以實現更好的偵錯,並提供可用於程式碼剖析等功能的掛鉤。
更多資源
#- 與 Zone 相關的 API 文件
- 閱讀 runZoned()、runZonedGuarded()、Zone、ZoneDelegate 和 ZoneSpecification 的文件。
- stack_trace
- 透過 stack_trace 函式庫的 Chain 類別,您可以為非同步執行的程式碼取得更好的堆疊追蹤。有關詳細資訊,請參閱 pub.dev 網站上的 stack_trace 套件。
更多範例
#以下是一些更複雜的 Zone 使用範例。
- task_interceptor 範例
- task_interceptor.dart 中的 toy zone 攔截
scheduleMicrotask
、createTimer
和createPeriodicTimer
,以模擬 Dart 原始型別的行為,而不會屈服於事件迴圈。 - stack_trace 套件的原始碼
- stack_trace 套件使用 Zone 來形成堆疊追蹤鏈,以偵錯非同步程式碼。使用的 Zone 功能包括錯誤處理、Zone 本機值和回呼。您可以在 stack_trace GitHub 專案中找到 stack_trace 原始碼。
- dart:async 的原始碼
- 這兩個 SDK 函式庫實作了以非同步回呼為特色的 API,因此它們處理 Zone。您可以在 sdk/lib 目錄下的 Dart GitHub 專案中瀏覽或下載其原始碼。
感謝 Anders Johnsen 和 Lasse Reichstein Nielsen 對本文的審閱。
除非另有說明,否則本網站上的文件反映 Dart 3.7.1。頁面最後更新於 2014-03-03。 檢視原始碼 或 回報問題。