Hello! My name is Dima and I am a frontend developer at Wrike. We write the client part of the project in Dart, but we have to work with asynchronous operations no less than with other technologies. Zones are one of the handy tools Dart provides for this. But in the Dart community you rarely find useful information about it, so I decided to understand and tell you more about this powerful tool.
Disclaimer: All code used in this article is only pretending to be copy-paste. In fact, I simplified it a lot and got rid of details that should not be paid attention to in the context of this article. In preparing the material, we used Dart version 2.7.2 and AngularDart version 5.0.0.
Dart . . , dart:async, .
Wrike ( AngularDart) :
// Part of AngularDart component class
final NgZone _zone;
final ChangeDetectorRef _detector;
final Element _element;
void onSomeLifecycleHook() {
_zone.runOutsideAngular(() {
_element.onMouseMove.where(filterEvent).listen((event) {
doWork(event);
_zone.run(_detector.markForCheck);
});
});
}
Dart- , . , .
, , , :
- API , .
- .
- ( , ).
Dart , issues github. API, , , , , DartUP. , .
, :
- package:intl;
- package:quiver;
- package:angular.
.
Intl
intl โ . : , , message plural .
:
class AppIntl {
static String loginLabel() => Intl.message(
'Login',
name: 'AppIntl_loginLabel',
);
}
. , , . , - . withLocale, :
// User has 'en' as default locale, but he works from Russia
final fallbackLocale = 'ru';
Future<Duration> parseText(String userText) async =>
// Try to parse user text
await _parseText(userText) ??
// Try to parse with 'ru' locale if default parsing failed
await Intl.withLocale(fallbackLocale, () => _parseText(userText));
// This is actual parser
Future<Duration> _parseText(String userText) {
// ...
}
-, fallback .
, withLocale , , . !
parseText Future, , . , - , . โ , . โ . .
, Future , , . .
1. Future
โ ! - Future:
class Future {
Future() : _zone = Zone.current; // Save current zone on creation
final Zone _zone;
// ...
}
Future . . , then:
class Future {
// ...
Future<R> then<R>(
FutureOr<R> callback(T value), // ...
) {
// Notify zone about callback for async operation
callback = Zone.current.registerUnaryCallback(callback);
final result = Future();
// Schedule to complete [result] when async operation ends
_addListener(_FutureListener.then(
result,
callback, // ...
));
return result;
}
}
class _FutureListener {
// ...
FutureOr<T> handleValue(S value) =>
// Call scheduled work inside zone that was saved in [result] Future
result._zone.runUnary(_callback, value);
}
! , . Future, . Future , โ Zone.current. runUnary . , , , . , - !
2. , , ยซ ยป
It's an execution context. โ Brian Ford, zone.js author.
โ , ยซยป: . , , . Future , , run*. -.
โ , _current. Zone.current โ _current. :
class Zone {
static Zone _current = _rootZone; // This is where current zone lives
static Zone get current => _current;
// ...
}
, . run* : run, runUnary, runBinary. _current:
class Zone {
// ...
R run<R>(R action()) {
Zone previous = _current;
// Place [this] zone in [_current] for a while
_current = this;
try {
return action(); // Then do stuff we wanted
} finally {
_current = previous; // Then revert current zone to previous
}
}
}
_current , . Zone.current .
! , , current , :
class _FutureListener {
// ...
FutureOr<T> handleValue(T value) => result._zone.runUnary(_callback, value);
}
class _FutureListener {
// ...
FutureOr<T> handleValue(T value) {
final previousZone = Zone.current;
Zone.current = result._zone;
final updatedValue = _callback(value);
Zone.current = previousZone;
return updatedValue;
}
}
. run* , , , . . .
, Intl , . - .
3. Intl
withLocale:
class Intl {
// ...
static withLocale(String locale, Function() callback) =>
// Create new zone with saved locale, then call callback inside it
runZoned(callback, zoneValues: {#Intl.locale: locale});
}
- ! .
runZoned . , runZoned . run* .
, โ zoneValues. , . zoneValues ( Symbol).
, :
class Intl {
// ...
static String getCurrentLocale() {
// Get locale from current zone
var zoneLocale = Zone.current[#Intl.locale];
return zoneLocale == null ? _defaultLocale : zoneLocale;
}
// ...
}
! , . , . - , .
[], ( โ ). []= โ , , . - withLocale runZoned:
class Intl {
// ...
static withLocale(String locale, Function() callback) =>
// Create new zone with saved locale, then call callback inside it
runZoned(callback, zoneValues: {#Intl.locale: locale});
}
, .
, :
// User has 'en' as default locale, but he works from Russia
final fallbackLocale = 'ru';
Future<Duration> parseText(String userText) async =>
// Try to parse user text
await _parseText(userText) ??
// Try to parse with 'ru' locale if default parsing failed
await Intl.withLocale(fallbackLocale, () => _parseText(userText));
// This is actual parser
Future<Duration> _parseText(String userText) {
// ...
}
, withLocale , . , Future . _parseText _parseText. !
, Future . Future, Stream Timer ยซยป . , . .
FakeAsync
- -. , . Dart . , test, . , Future test, expect, Future :
void main() {
test('do some testing', () {
return getAsyncResult().then((result) {
expect(result, isTrue);
});
});
}
โ . , debounce , . ยซยป mock , .
, . , . package:quiver FakeAsync.
:
import 'package:quiver/testing/async.dart';
void main() {
test('do some testing', () {
// Make FakeAsync object and run async code with it
FakeAsync().run((fakeAsync) {
getAsyncResult().then((result) {
expect(result, isTrue);
});
// Ask FakeAsync to flush all timers and microtasks
fakeAsync.flushTimers();
});
});
}
FakeAsync, , . .
, .
1. FakeAsync
run :
class FakeAsync {
// ...
dynamic run(callback(FakeAsync self)) {
// Make new zone if there wasn't any zone created before
_zone ??= Zone.current.fork(specification: _zoneSpec);
dynamic result;
// Call the test callback inside custom zone
_zone.runGuarded(() {
result = callback(this);
});
return result;
}
}
โ , .
โ fork specification.
Dart โ root. , โ Zone.root. root, root . run, ?
class Zone {
// ...
R run<R>(R action()) {
Zone previous = _current;
// Place [this] zone in [_current] for a while
_current = this;
try {
return action(); // Then do stuff we wanted
} finally {
_current = previous; // Then revert current zone to previous
}
}
}
ยซ ยป. :
class _RootZone implements Zone {
// Only root zone can change current zone
// ...
R _run<R>(Zone self, ZoneDelegate parent, Zone zone, R action()) {
Zone previous = Zone._current;
// On this [zone] the .run() method was initially called
Zone._current = zone;
try {
return action(); // Then do stuff we wanted
} finally {
Zone._current = previous; // Then revert current zone to previous
}
}
}
!
โ root . , - .
2. zoneSpecification
ZoneSpecification โ zoneValues:
abstract class ZoneSpecification {
// All this handlers can be added during object creation
// ...
HandleUncaughtErrorHandler get handleUncaughtError;
RunHandler get run;
RunUnaryHandler get runUnary;
RunBinaryHandler get runBinary;
RegisterCallbackHandler get registerCallback;
RegisterUnaryCallbackHandler get registerUnaryCallback;
RegisterBinaryCallbackHandler get registerBinaryCallback;
ErrorCallbackHandler get errorCallback;
ScheduleMicrotaskHandler get scheduleMicrotask;
CreateTimerHandler get createTimer;
CreatePeriodicTimerHandler get createPeriodicTimer;
PrintHandler get print;
ForkHandler get fork;
}
, , . โ -. , .
โ , - :
// This is the actual type of run handler
typedef RunHandler = R Function<R>(
Zone self, // Reference to the zone with this specification
ZoneDelegate parent, // Object for delegating work to [self] parent zone
Zone zone, // On this zone .run() method was initially called
R Function() action, // The actual work we want to run in [zone]
);
int _counter = 0;
final zone = Zone.current.fork(
specification: ZoneSpecification(
// This will be called within [zone.run(doWork);]
run: <R>(self, parent, zone, action) {
// RunHandler
// Delegate an updated work to parent, so in addition
// to the work being done, the counter will also increase
parent.run(zone, () {
_counter += 1;
action();
});
},
),
);
void main() {
zone.run(doWork);
}
. run , run. - โ , .
.
, . , -. .
. - , ยซยป . , , , , root . , .
โ , .
โ , ยซยป . , root , _current .
โ , . , , , . , .
, . :
: , , D B
, .
3. - FakeAsync
FakeAsync. , run , . :
class FakeAsync {
// ...
ZoneSpecification get _zoneSpec => ZoneSpecification(
// ...
scheduleMicrotask: (_, __, ___, Function microtask) {
_microtasks.add(microtask); // Just save callback
},
createTimer: (_, __, ___, Duration duration, Function callback) {
// Save timer that can immediately provide its callback to us
var timer = _FakeTimer._(duration, callback, isPeriodic, this);
_timers.add(timer);
return timer;
},
);
}
scheduleMicrotask. , - , . , Future , Future . : Stream .
FakeAsync c โ .
createTimer. createTimer, , , Timer. : ยซ ?ยป. :
abstract class Timer {
factory Timer(Duration duration, void callback()) {
// Create timer with current zone
return Zone.current
.createTimer(duration, Zone.current.bindCallbackGuarded(callback));
}
// ...
// Create timer from environment
external static Timer _createTimer(Duration duration, void callback());
}
class _RootZone implements Zone {
// ...
Timer createTimer(Duration duration, void f()) {
return Timer._createTimer(duration, f);
}
}
โ , . , , . FakeAsync: _FakeTimer, โ .
class FakeAsync {
// ...
ZoneSpecification get _zoneSpec => ZoneSpecification(
// ...
scheduleMicrotask: (_, __, ___, Function microtask) {
_microtasks.add(microtask); // Just save callback
},
createTimer: (_, __, ___, Duration duration, Function callback) {
// Save timer that can immediately provide its callback to us
var timer = _FakeTimer._(duration, callback, isPeriodic, this);
_timers.add(timer);
return timer;
},
);
}
run, . , . , . FakeAsync โ , .
, ! flushTimers:
class FakeAsync {
// ...
void flushTimers() {
// Call timer callback for every saved timer
while (_timers.isNotEmpty) {
final timer = _timers.removeFirst();
timer._callback(timer);
// Call every microtask after processing each timer
_drainMicrotasks();
}
}
void _drainMicrotasks() {
while (_microtasks.isNotEmpty) {
final microtask = _microtasks.removeFirst();
microtask();
}
}
}
, , , . , !
, , ZoneSpecification. .
:
- (handleUncaughtError, errorCallback);
- (registerCallback*);
- (createPeriodicTimer);
- (print);
- (fork).
!