Olá! Meu nome é Dima e sou desenvolvedor front-end da Wrike. Escrevemos a parte do cliente do projeto no Dart, mas temos que trabalhar com operações assíncronas não menos que com outras tecnologias. As zonas são uma das ferramentas úteis que o Dart fornece para isso. Mas na comunidade Dart você raramente encontra informações úteis sobre isso, então decidi entender e falar mais sobre essa ferramenta poderosa.
Isenção de responsabilidade: todo o código usado neste artigo está apenas fingindo ser copiar e colar. De fato, simplifiquei bastante e me livrei de detalhes que não deveriam ser prestados atenção no contexto deste artigo. Na preparação do material, usamos o Dart versão 2.7.2 e o AngularDart versão 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).
!