Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

Commit d4a1436

Browse files
juliemrmhevery
authored andcommitted
fix(tasks): do not drain the microtask queue early.
Fixes a bug where a event task invoked inside another task would drain the microtask queue too early. This would mean that microtasks would be called unexpectedly in the middle of what should have been a block of synchronous code.
1 parent 5e3c207 commit d4a1436

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

Diff for: lib/zone.ts

+10-4
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ const Zone: ZoneType = (function(global: any) {
504504
if (global.Zone) {
505505
throw new Error('Zone already loaded.');
506506
}
507+
507508
class Zone implements AmbientZone {
508509
static __symbol__: (name: string) => string = __symbol__;
509510

@@ -835,10 +836,14 @@ const Zone: ZoneType = (function(global: any) {
835836
this.callback = callback;
836837
const self = this;
837838
this.invoke = function () {
839+
_numberOfNestedTaskFrames++;
838840
try {
839841
return zone.runTask(self, this, <any>arguments);
840842
} finally {
841-
drainMicroTaskQueue();
843+
if (_numberOfNestedTaskFrames == 1) {
844+
drainMicroTaskQueue();
845+
}
846+
_numberOfNestedTaskFrames--;
842847
}
843848
};
844849
}
@@ -869,10 +874,12 @@ const Zone: ZoneType = (function(global: any) {
869874
let _microTaskQueue: Task[] = [];
870875
let _isDrainingMicrotaskQueue: boolean = false;
871876
const _uncaughtPromiseErrors: UncaughtPromiseError[] = [];
872-
let _drainScheduled: boolean = false;
877+
let _numberOfNestedTaskFrames = 0;
873878

874879
function scheduleQueueDrain() {
875-
if (!_drainScheduled && !_currentTask && _microTaskQueue.length == 0) {
880+
// if we are not running in any task, and there has not been anything scheduled
881+
// we must bootstrap the initial task creation by manually scheduling the drain
882+
if (_numberOfNestedTaskFrames == 0 && _microTaskQueue.length == 0) {
876883
// We are not running in Task, so we need to kickstart the microtask queue.
877884
if (global[symbolPromise]) {
878885
global[symbolPromise].resolve(0)[symbolThen](drainMicroTaskQueue);
@@ -927,7 +934,6 @@ const Zone: ZoneType = (function(global: any) {
927934
}
928935
}
929936
_isDrainingMicrotaskQueue = false;
930-
_drainScheduled = false;
931937
}
932938
}
933939

Diff for: test/browser/element.spec.ts

+42
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,48 @@ describe('element', function () {
5050
button.dispatchEvent(clickEvent);
5151
});
5252

53+
it('should not call microtasks early when an event is invoked', function(done) {
54+
// we have to run this test in setTimeout to guarantee that we are running in an existing task
55+
setTimeout(() => {
56+
var log = '';
57+
Zone.current.scheduleMicroTask('test', () => log += 'microtask;');
58+
button.addEventListener('click', () => log += 'click;');
59+
button.click();
60+
61+
expect(log).toEqual('click;');
62+
done();
63+
});
64+
});
65+
66+
it('should call microtasks early when an event is invoked', function(done) {
67+
/*
68+
* In this test we escape the Zone using unpatched setTimeout.
69+
* This way the eventTask invoked from click will think it is the top most
70+
* task and eagerly drain the microtask queue.
71+
*
72+
* THIS IS THE WRONG BEHAVIOR!
73+
*
74+
* But there is no easy way for the task to know if it is the top most task.
75+
*
76+
* Given that this can only arise when someone is emulating clicks on DOM in a synchronous
77+
* fashion we have few choices:
78+
* 1. Ignore as this is unlikely to be a problem outside of tests.
79+
* 2. Monkey patch the event methods to increment the _numberOfNestedTaskFrames and prevent
80+
* eager drainage.
81+
* 3. Pay the cost of throwing an exception in event tasks and verifying that we are the
82+
* top most frame.
83+
*/
84+
global[Zone['__symbol__']('setTimeout')](() => {
85+
var log = '';
86+
Zone.current.scheduleMicroTask('test', () => log += 'microtask;');
87+
button.addEventListener('click', () => log += 'click;');
88+
button.click();
89+
90+
expect(log).toEqual('click;microtask;');
91+
done();
92+
});
93+
});
94+
5395
it('should work with addEventListener when called with an EventListener-implementing listener', function () {
5496
var eventListener = {
5597
x: 5,

Diff for: test/common/zone.spec.ts

+27
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,33 @@ describe('Zone', function () {
150150
]);
151151
});
152152
});
153+
154+
describe('invoking tasks', () => {
155+
var log;
156+
function noop() {}
157+
158+
159+
beforeEach(() => {
160+
log = [];
161+
});
162+
163+
it('should not drain the microtask queue too early', () => {
164+
var z = Zone.current;
165+
var event = z.scheduleEventTask('test', () => log.push('eventTask'), null, noop, noop);
166+
167+
z.scheduleMicroTask('test', () => log.push('microTask'));
168+
169+
var macro = z.scheduleMacroTask('test', () => {
170+
event.invoke();
171+
// At this point, we should not have invoked the microtask.
172+
expect(log).toEqual([
173+
'eventTask'
174+
]);
175+
}, null, noop, noop);
176+
177+
macro.invoke();
178+
});
179+
});
153180
});
154181

155182
function throwError () {

0 commit comments

Comments
 (0)