Skip to content

Commit fc72ee1

Browse files
committed
feat(Zone): add onTurnStart to NgZone.
OnTurnStart is executed at the beginning of every turn. Any microtasks scheduled in onTurnStart are executed before the ones scheduled in run. Closes dart-archive#83
1 parent 9622318 commit fc72ee1

File tree

2 files changed

+141
-34
lines changed

2 files changed

+141
-34
lines changed

lib/core/zone.dart

+37-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,12 @@ part of angular.core_internal;
33
/**
44
* Handles an [NgZone] onTurnDone event.
55
*/
6-
typedef void ZoneOnTurn();
6+
typedef void ZoneOnTurnDone();
7+
8+
/**
9+
* Handles an [NgZone] onTurnDone event.
10+
*/
11+
typedef void ZoneOnTurnStart();
712

813
/**
914
* Handles an [NgZone] onError event.
@@ -69,14 +74,20 @@ class VmTurnZone {
6974
));
7075
onError = _defaultOnError;
7176
onTurnDone = _defaultOnTurnDone;
77+
onTurnStart = _defaultOnTurnStart;
7278
}
7379

7480
List _asyncQueue = [];
7581
bool _errorThrownFromOnRun = false;
7682

83+
var _currentlyInTurn = false;
7784
_onRunBase(async.Zone self, async.ZoneDelegate delegate, async.Zone zone, fn()) {
7885
_runningInTurn++;
7986
try {
87+
if (!_currentlyInTurn) {
88+
_currentlyInTurn = true;
89+
delegate.run(zone, onTurnStart);
90+
}
8091
return fn();
8192
} catch (e, s) {
8293
onError(e, s, _longStacktrace);
@@ -115,11 +126,18 @@ class VmTurnZone {
115126
// Two loops here: the inner one runs all queued microtasks,
116127
// the outer runs onTurnDone (e.g. scope.digest) and then
117128
// any microtasks which may have been queued from onTurnDone.
129+
// If any microtasks were scheduled during onTurnDone, onTurnStart
130+
// will be executed before those microtasks.
118131
do {
132+
if (!_currentlyInTurn) {
133+
_currentlyInTurn = true;
134+
delegate.run(zone, onTurnStart);
135+
}
119136
while (!_asyncQueue.isEmpty) {
120137
delegate.run(zone, _asyncQueue.removeAt(0));
121138
}
122139
delegate.run(zone, onTurnDone);
140+
_currentlyInTurn = false;
123141
} while (!_asyncQueue.isEmpty);
124142
} catch (e, s) {
125143
onError(e, s, _longStacktrace);
@@ -141,18 +159,31 @@ class VmTurnZone {
141159
void _defaultOnError(dynamic e, dynamic s, LongStackTrace ls) =>
142160
_outerZone.handleUncaughtError(e, s);
143161

162+
/**
163+
* Called at the beginning of each VM turn in which inner zone code runs.
164+
* "At the beginning" means before any of the microtasks from the private
165+
* microtask queue of the inner zone is executed. Notes
166+
* - [onTurnStart] runs repeatedly until no more microstasks are scheduled
167+
* within [onTurnStart], [run] or [onTurnDone]. You usually don't want it to
168+
* schedule any. For example, if its first line of code is `new Future.value()`,
169+
* the turn will _never_ end.
170+
*/
171+
ZoneOnTurnStart onTurnStart;
172+
void _defaultOnTurnStart() => null;
173+
174+
144175
/**
145176
* Called at the end of each VM turn in which inner zone code runs.
146177
* "At the end" means after the private microtask queue of the inner zone is
147178
* exhausted but before the next VM turn. Notes
148179
* - This won't wait for microtasks scheduled in zones other than the inner
149180
* zone, e.g. those scheduled with [runOutsideAngular].
150-
* - [onTurnDone] runs repeatedly until it fails to schedule any more
151-
* microtasks, so you usually don't want it to schedule any. For example,
152-
* if its first line of code is `new Future.value()`, the turn will _never_
153-
* end.
181+
* - [onTurnDone] runs repeatedly until no more tasks are scheduled within
182+
* [onTurnStart], [run] or [onTurnDone]. You usually don't want it to
183+
* schedule any. For example, if its first line of code is `new Future.value()`,
184+
* the turn will _never_ end.
154185
*/
155-
ZoneOnTurn onTurnDone;
186+
ZoneOnTurnDone onTurnDone;
156187
void _defaultOnTurnDone() => null;
157188

158189
LongStackTrace _longStacktrace = null;

test/core/zone_spec.dart

+104-28
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ void main() {
1818
zone.onTurnDone = () {
1919
log('onTurnDone');
2020
};
21+
zone.onTurnStart = () {
22+
log('onTurnStart');
23+
};
2124
zone.onError = (e, s, ls) => eh(e, s);
2225
});
2326

@@ -139,7 +142,7 @@ void main() {
139142
zone.run(() {
140143
log('run');
141144
});
142-
expect(log.result()).toEqual('run; onTurnDone');
145+
expect(log.result()).toEqual('onTurnStart; run; onTurnDone');
143146
});
144147

145148

@@ -148,43 +151,52 @@ void main() {
148151
});
149152

150153

151-
it('should call onTurnDone for a scheduleMicrotask in onTurnDone', async((Logger log) {
154+
it('should call onTurnStart before executing a microtask scheduled in onTurnDone as well as '
155+
'onTurnDone after executing the task', async((Logger log) {
152156
var ran = false;
153157
zone.onTurnDone = () {
158+
log('onTurnDone(begin)');
154159
if (!ran) {
155-
scheduleMicrotask(() { ran = true; log('onTurnAsync'); });
160+
scheduleMicrotask(() { ran = true; log('executedMicrotask'); });
156161
}
157-
log('onTurnDone');
162+
log('onTurnDone(end)');
158163
};
159164
zone.run(() {
160165
log('run');
161166
});
162167
microLeap();
163168

164-
expect(log.result()).toEqual('run; onTurnDone; onTurnAsync; onTurnDone');
169+
expect(log.result()).toEqual('onTurnStart; run; onTurnDone(begin); onTurnDone(end); onTurnStart; executedMicrotask; onTurnDone(begin); onTurnDone(end)');
165170
}));
166171

167172

168-
it('should call onTurnDone for a scheduleMicrotask in onTurnDone triggered by a scheduleMicrotask in run', async((Logger log) {
173+
it('should call onTurnStart and onTurnDone for a scheduleMicrotask in onTurnDone triggered by a scheduleMicrotask in run', async((Logger log) {
169174
var ran = false;
170175
zone.onTurnDone = () {
176+
log('onTurnDone(begin)');
171177
if (!ran) {
172-
scheduleMicrotask(() { ran = true; log('onTurnAsync'); });
178+
log('onTurnDone(scheduleMicrotask)');
179+
scheduleMicrotask(() {
180+
ran = true;
181+
log('onTurnDone(executeMicrotask)');
182+
});
173183
}
174-
log('onTurnDone');
184+
log('onTurnDone(end)');
175185
};
176186
zone.run(() {
177-
scheduleMicrotask(() { log('scheduleMicrotask'); });
178-
log('run');
187+
log('scheduleMicrotask');
188+
scheduleMicrotask(() {
189+
log('run(executeMicrotask)');
190+
});
179191
});
180192
microLeap();
181193

182-
expect(log.result()).toEqual('run; scheduleMicrotask; onTurnDone; onTurnAsync; onTurnDone');
194+
expect(log.result()).toEqual('onTurnStart; scheduleMicrotask; run(executeMicrotask); onTurnDone(begin); onTurnDone(scheduleMicrotask); onTurnDone(end); onTurnStart; onTurnDone(executeMicrotask); onTurnDone(begin); onTurnDone(end)');
183195
}));
184196

185197

186198

187-
it('should call onTurnDone once after a turn', async((Logger log) {
199+
it('should call onTurnStart once before a turn and onTurnDone once after the turn', async((Logger log) {
188200
zone.run(() {
189201
log('run start');
190202
scheduleMicrotask(() {
@@ -194,18 +206,61 @@ void main() {
194206
});
195207
microLeap();
196208

197-
expect(log.result()).toEqual('run start; run end; async; onTurnDone');
209+
expect(log.result()).toEqual('onTurnStart; run start; run end; async; onTurnDone');
198210
}));
199211

200212

201213
it('should work for Future.value as well', async((Logger log) {
202214
var futureRan = false;
203215
zone.onTurnDone = () {
216+
log('onTurnDone(begin)');
204217
if (!futureRan) {
205-
new Future.value(null).then((_) { log('onTurn future'); });
218+
log('onTurnDone(scheduleFuture)');
219+
new Future.value(null).then((_) { log('onTurnDone(executeFuture)'); });
206220
futureRan = true;
207221
}
208-
log('onTurnDone');
222+
log('onTurnDone(end)');
223+
};
224+
225+
zone.run(() {
226+
log('run start');
227+
new Future.value(null)
228+
.then((_) {
229+
log('future then');
230+
new Future.value(null)
231+
.then((_) { log('future foo'); });
232+
return new Future.value(null);
233+
})
234+
.then((_) {
235+
log('future bar');
236+
});
237+
log('run end');
238+
});
239+
microLeap();
240+
241+
expect(log.result()).toEqual('onTurnStart; run start; run end; future then; future foo; future bar; onTurnDone(begin); onTurnDone(scheduleFuture); onTurnDone(end); onTurnStart; onTurnDone(executeFuture); onTurnDone(begin); onTurnDone(end)');
242+
}));
243+
244+
it('should execute futures scheduled in onTurnStart before Futures scheduled in run', async((Logger log) {
245+
var doneFutureRan = false;
246+
var startFutureRan = false;
247+
zone.onTurnStart = () {
248+
log('onTurnStart(begin)');
249+
if (!startFutureRan) {
250+
log('onTurnStart(scheduleFuture)');
251+
new Future.value(null).then((_) { log('onTurnStart(executeFuture)'); });
252+
startFutureRan = true;
253+
}
254+
log('onTurnStart(end)');
255+
};
256+
zone.onTurnDone = () {
257+
log('onTurnDone(begin)');
258+
if (!doneFutureRan) {
259+
log('onTurnDone(scheduleFuture)');
260+
new Future.value(null).then((_) { log('onTurnDone(executeFuture)'); });
261+
doneFutureRan = true;
262+
}
263+
log('onTurnDone(end)');
209264
};
210265

211266
zone.run(() {
@@ -214,21 +269,21 @@ void main() {
214269
.then((_) {
215270
log('future then');
216271
new Future.value(null)
217-
.then((_) { log('future ?'); });
272+
.then((_) { log('future foo'); });
218273
return new Future.value(null);
219274
})
220275
.then((_) {
221-
log('future ?');
276+
log('future bar');
222277
});
223278
log('run end');
224279
});
225280
microLeap();
226281

227-
expect(log.result()).toEqual('run start; run end; future then; future ?; future ?; onTurnDone; onTurn future; onTurnDone');
282+
expect(log.result()).toEqual('onTurnStart(begin); onTurnStart(scheduleFuture); onTurnStart(end); run start; run end; onTurnStart(executeFuture); future then; future foo; future bar; onTurnDone(begin); onTurnDone(scheduleFuture); onTurnDone(end); onTurnStart(begin); onTurnStart(end); onTurnDone(executeFuture); onTurnDone(begin); onTurnDone(end)');
228283
}));
229284

230285

231-
it('should call onTurnDone after each turn', async((Logger log) {
286+
it('should call onTurnStart and onTurnDone before and after each turn, respectively', async((Logger log) {
232287
Completer a, b;
233288
zone.run(() {
234289
a = new Completer();
@@ -247,11 +302,11 @@ void main() {
247302
});
248303
microLeap();
249304

250-
expect(log.result()).toEqual('run start; onTurnDone; a then; onTurnDone; b then; onTurnDone');
305+
expect(log.result()).toEqual('onTurnStart; run start; onTurnDone; onTurnStart; a then; onTurnDone; onTurnStart; b then; onTurnDone');
251306
}));
252307

253308

254-
it('should call onTurnDone after each turn in a chain', async((Logger log) {
309+
it('should call onTurnStart and onTurnDone before and after (respectively) all turns in a chain', async((Logger log) {
255310
zone.run(() {
256311
log('run start');
257312
scheduleMicrotask(() {
@@ -264,18 +319,18 @@ void main() {
264319
});
265320
microLeap();
266321

267-
expect(log.result()).toEqual('run start; run end; async1; async2; onTurnDone');
322+
expect(log.result()).toEqual('onTurnStart; run start; run end; async1; async2; onTurnDone');
268323
}));
269324

270-
it('should call onTurnDone for futures created outside of run body', async((Logger log) {
325+
it('should call onTurnStart and onTurnDone for futures created outside of run body', async((Logger log) {
271326
var future = new Future.value(4).then((x) => new Future.value(x));
272327
zone.run(() {
273328
future.then((_) => log('future then'));
274329
log('zone run');
275330
});
276331
microLeap();
277332

278-
expect(log.result()).toEqual('zone run; onTurnDone; future then; onTurnDone');
333+
expect(log.result()).toEqual('onTurnStart; zone run; onTurnDone; onTurnStart; future then; onTurnDone');
279334
}));
280335

281336

@@ -286,7 +341,20 @@ void main() {
286341
throw 'zoneError';
287342
})).toThrow('zoneError');
288343
expect(() => zone.assertInTurn()).toThrow();
289-
expect(log.result()).toEqual('zone run; onError; onTurnDone');
344+
expect(log.result()).toEqual('onTurnStart; zone run; onError; onTurnDone');
345+
}));
346+
347+
it('should call onTurnDone even if there was an exception in onTurnStart', async((Logger log) {
348+
zone.onError = (e, s, l) => log('onError');
349+
zone.onTurnStart = (){
350+
log('onTurnStart');
351+
throw 'zoneError';
352+
};
353+
expect(() => zone.run(() {
354+
log('zone run');
355+
})).toThrow('zoneError');
356+
expect(() => zone.assertInTurn()).toThrow();
357+
expect(log.result()).toEqual('onTurnStart; onError; onTurnDone');
290358
}));
291359

292360

@@ -303,11 +371,15 @@ void main() {
303371
microLeap();
304372

305373
expect(() => zone.assertInTurn()).toThrow();
306-
expect(log.result()).toEqual('zone run; scheduleMicrotask; onError; onTurnDone');
374+
expect(log.result()).toEqual('onTurnStart; zone run; scheduleMicrotask; onError; onTurnDone');
307375
}));
308376

309377
it('should support assertInZone', async(() {
310378
var calls = '';
379+
zone.onTurnStart = () {
380+
zone.assertInZone();
381+
calls += 'start;';
382+
};
311383
zone.onTurnDone = () {
312384
zone.assertInZone();
313385
calls += 'done;';
@@ -322,7 +394,7 @@ void main() {
322394
});
323395

324396
microLeap();
325-
expect(calls).toEqual('sync;async;done;');
397+
expect(calls).toEqual('start;sync;async;done;');
326398
}));
327399

328400
it('should throw outside of the zone', () {
@@ -335,6 +407,10 @@ void main() {
335407

336408
it('should support assertInTurn', async(() {
337409
var calls = '';
410+
zone.onTurnStart = () {
411+
zone.assertInTurn();
412+
calls += 'start;';
413+
};
338414
zone.onTurnDone = () {
339415
calls += 'done;';
340416
zone.assertInTurn();
@@ -349,7 +425,7 @@ void main() {
349425
});
350426

351427
microLeap();
352-
expect(calls).toEqual('sync;async;done;');
428+
expect(calls).toEqual('start;sync;async;done;');
353429
}));
354430

355431

0 commit comments

Comments
 (0)