Skip to content

Commit 27690de

Browse files
author
Robert Jackson
committed
Use microtask queue to flush autoruns.
* Replace the private `_platform` option, with a new `_buildPlatform` * Add interfaces for the constructor args for the `Backburner` class. * Move the main implementation of `Backburner.prototype.end` into a private method (`_end`) that is aware of if the end is from an autorun or manual run... * Extract steps to schedule an autorun out into stand alone private method (`_scheduleAutorun`) * Leverage microtask queue (via either `promise.then(...)` or `MutationObserver`) to schedule an autorun * Leverage microtask queue to advance from one queue to the next
1 parent e65437d commit 27690de

9 files changed

+280
-150
lines changed

lib/backburner/deferred-action-queues.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export default class DeferredActionQueues {
4848
@method flush
4949
DeferredActionQueues.flush() calls Queue.flush()
5050
*/
51-
public flush() {
51+
public flush(fromAutorun = false) {
5252
let queue;
5353
let queueName;
5454
let numberOfQueues = this.queueNames.length;
@@ -59,6 +59,9 @@ export default class DeferredActionQueues {
5959

6060
if (queue.hasWork() === false) {
6161
this.queueNameIndex++;
62+
if (fromAutorun) {
63+
return QUEUE_STATE.Pause;
64+
}
6265
} else {
6366
if (queue.flush(false /* async */) === QUEUE_STATE.Pause) {
6467
return QUEUE_STATE.Pause;

lib/backburner/platform.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export interface IPlatform {
2+
setTimeout(fn: Function, ms: number): any;
3+
clearTimeout(id: any): void;
4+
next(): any;
5+
clearNext(timerId: any): void;
6+
now(): number;
7+
}
8+
9+
const SET_TIMEOUT = setTimeout;
10+
const NOOP = () => {};
11+
12+
export function buildPlatform(flush: () => void): IPlatform {
13+
let next;
14+
let clearNext = NOOP;
15+
16+
if (typeof MutationObserver === 'function') {
17+
let iterations = 0;
18+
let observer = new MutationObserver(flush);
19+
let node = document.createTextNode('');
20+
observer.observe(node, { characterData: true });
21+
22+
next = () => {
23+
iterations = ++iterations % 2;
24+
node.data = '' + iterations;
25+
return iterations;
26+
};
27+
28+
} else if (typeof Promise === 'function') {
29+
const autorunPromise = Promise.resolve();
30+
next = () => autorunPromise.then(flush);
31+
32+
} else {
33+
next = () => SET_TIMEOUT(flush, 0);
34+
}
35+
36+
return {
37+
setTimeout(fn, ms) {
38+
return SET_TIMEOUT(fn, ms);
39+
},
40+
41+
clearTimeout(timerId: number) {
42+
return clearTimeout(timerId);
43+
},
44+
45+
now() {
46+
return Date.now();
47+
},
48+
49+
next,
50+
clearNext,
51+
};
52+
}

lib/backburner/queue.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default class Queue {
3333
}
3434
}
3535

36-
public flush(sync?) {
36+
public flush(sync?: Boolean) {
3737
let { before, after } = this.options;
3838
let target;
3939
let method;

lib/index.ts

+85-64
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
export {
2+
buildPlatform,
3+
IPlatform
4+
} from './backburner/platform';
5+
6+
import {
7+
buildPlatform,
8+
IPlatform,
9+
} from './backburner/platform';
110
import {
211
findItem,
312
findTimer,
@@ -14,7 +23,6 @@ import Queue, { QUEUE_STATE } from './backburner/queue';
1423
type Timer = any;
1524

1625
const noop = function() {};
17-
const SET_TIMEOUT = setTimeout;
1826

1927
function parseArgs() {
2028
let length = arguments.length;
@@ -126,14 +134,25 @@ let autorunsCompletedCount = 0;
126134
let deferredActionQueuesCreatedCount = 0;
127135
let nestedDeferredActionQueuesCreated = 0;
128136

137+
export interface IBackburnerOptions {
138+
defaultQueue?: string;
139+
onBegin?: (currentInstance: DeferredActionQueues, previousInstance: DeferredActionQueues) => void;
140+
onEnd?: (currentInstance: DeferredActionQueues, nextInstance: DeferredActionQueues) => void;
141+
onError?: (error: any, errorRecordedForStack?: any) => void;
142+
onErrorTarget?: any;
143+
onErrorMethod?: string;
144+
mustYield?: () => boolean;
145+
_buildPlatform?: (flush: () => void) => IPlatform;
146+
}
147+
129148
export default class Backburner {
130149
public static Queue = Queue;
131150

132151
public DEBUG = false;
133152

134153
public currentInstance: DeferredActionQueues | null = null;
135154

136-
public options: any;
155+
public options: IBackburnerOptions;
137156

138157
public get counters() {
139158
return {
@@ -183,49 +202,45 @@ export default class Backburner {
183202

184203
private _timerTimeoutId: number | null = null;
185204
private _timers: any[] = [];
186-
private _platform: {
187-
setTimeout(fn: Function, ms: number): number;
188-
clearTimeout(id: number): void;
189-
next(fn: Function): number;
190-
clearNext(id: any): void;
191-
now(): number;
192-
};
205+
private _platform: IPlatform;
193206

194207
private _boundRunExpiredTimers: () => void;
195208

196209
private _autorun: number | null = null;
197210
private _boundAutorunEnd: () => void;
211+
private _defaultQueue: string;
198212

199-
constructor(queueNames: string[], options: any = {} ) {
213+
constructor(queueNames: string[], options?: IBackburnerOptions) {
200214
this.queueNames = queueNames;
201-
this.options = options;
202-
if (!this.options.defaultQueue) {
203-
this.options.defaultQueue = queueNames[0];
215+
this.options = options || {};
216+
if (typeof this.options.defaultQueue === 'string') {
217+
this._defaultQueue = this.options.defaultQueue;
218+
} else {
219+
this._defaultQueue = this.queueNames[0];
204220
}
205221

206222
this._onBegin = this.options.onBegin || noop;
207223
this._onEnd = this.options.onEnd || noop;
208224

209-
let _platform = this.options._platform || {};
210-
let platform = Object.create(null);
211-
212-
platform.setTimeout = _platform.setTimeout || ((fn, ms) => setTimeout(fn, ms));
213-
platform.clearTimeout = _platform.clearTimeout || ((id) => clearTimeout(id));
214-
platform.next = _platform.next || ((fn) => SET_TIMEOUT(fn, 0));
215-
platform.clearNext = _platform.clearNext || platform.clearTimeout;
216-
platform.now = _platform.now || (() => Date.now());
217-
218-
this._platform = platform;
219-
220225
this._boundRunExpiredTimers = this._runExpiredTimers.bind(this);
221226

222227
this._boundAutorunEnd = () => {
223228
autorunsCompletedCount++;
229+
230+
// if the autorun was already flushed, do nothing
231+
if (this._autorun === null) { return; }
232+
224233
this._autorun = null;
225-
this.end();
234+
this._end(true /* fromAutorun */);
226235
};
236+
237+
let builder = this.options._buildPlatform || buildPlatform;
238+
this._platform = builder(this._boundAutorunEnd);
227239
}
228240

241+
public get defaultQueue() {
242+
return this._defaultQueue;
243+
}
229244
/*
230245
@method begin
231246
@return instantiated class DeferredActionQueues
@@ -257,40 +272,7 @@ export default class Backburner {
257272

258273
public end() {
259274
endCount++;
260-
let currentInstance = this.currentInstance;
261-
let nextInstance: DeferredActionQueues | null = null;
262-
263-
if (currentInstance === null) {
264-
throw new Error(`end called without begin`);
265-
}
266-
267-
// Prevent double-finally bug in Safari 6.0.2 and iOS 6
268-
// This bug appears to be resolved in Safari 6.0.5 and iOS 7
269-
let finallyAlreadyCalled = false;
270-
let result;
271-
try {
272-
result = currentInstance.flush();
273-
} finally {
274-
if (!finallyAlreadyCalled) {
275-
finallyAlreadyCalled = true;
276-
277-
if (result === QUEUE_STATE.Pause) {
278-
autorunsCreatedCount++;
279-
const next = this._platform.next;
280-
this._autorun = next(this._boundAutorunEnd);
281-
} else {
282-
this.currentInstance = null;
283-
284-
if (this.instanceStack.length > 0) {
285-
nextInstance = this.instanceStack.pop() as DeferredActionQueues;
286-
this.currentInstance = nextInstance;
287-
}
288-
endEventCount++;
289-
this._trigger('end', currentInstance, nextInstance);
290-
this._onEnd(currentInstance, nextInstance);
291-
}
292-
}
293-
}
275+
this._end(false);
294276
}
295277

296278
public on(eventName, callback) {
@@ -564,6 +546,40 @@ export default class Backburner {
564546
this._ensureInstance();
565547
}
566548

549+
private _end(fromAutorun: boolean) {
550+
let currentInstance = this.currentInstance;
551+
let nextInstance: DeferredActionQueues | null = null;
552+
553+
if (currentInstance === null) {
554+
throw new Error(`end called without begin`);
555+
}
556+
557+
// Prevent double-finally bug in Safari 6.0.2 and iOS 6
558+
// This bug appears to be resolved in Safari 6.0.5 and iOS 7
559+
let finallyAlreadyCalled = false;
560+
let result;
561+
try {
562+
result = currentInstance.flush(fromAutorun);
563+
} finally {
564+
if (!finallyAlreadyCalled) {
565+
finallyAlreadyCalled = true;
566+
567+
if (result === QUEUE_STATE.Pause) {
568+
this._scheduleAutorun();
569+
} else {
570+
this.currentInstance = null;
571+
572+
if (this.instanceStack.length > 0) {
573+
nextInstance = this.instanceStack.pop() as DeferredActionQueues;
574+
this.currentInstance = nextInstance;
575+
}
576+
this._trigger('end', currentInstance, nextInstance);
577+
this._onEnd(currentInstance, nextInstance);
578+
}
579+
}
580+
}
581+
}
582+
567583
private _join(target, method, args) {
568584
if (this.currentInstance === null) {
569585
return this._run(target, method, args);
@@ -654,7 +670,7 @@ export default class Backburner {
654670
/**
655671
Trigger an event. Supports up to two arguments. Designed around
656672
triggering transition events from one run loop instance to the
657-
next, which requires an argument for the first instance and then
673+
next, which requires an argument for the instance and then
658674
an argument for the next instance.
659675
660676
@private
@@ -685,7 +701,7 @@ export default class Backburner {
685701
let timers = this._timers;
686702
let i = 0;
687703
let l = timers.length;
688-
let defaultQueue = this.options.defaultQueue;
704+
let defaultQueue = this._defaultQueue;
689705
let n = this._platform.now();
690706

691707
for (; i < l; i += 6) {
@@ -725,11 +741,16 @@ export default class Backburner {
725741
private _ensureInstance(): DeferredActionQueues {
726742
let currentInstance = this.currentInstance;
727743
if (currentInstance === null) {
728-
autorunsCreatedCount++;
729744
currentInstance = this.begin();
730-
const next = this._platform.next;
731-
this._autorun = next(this._boundAutorunEnd);
745+
this._scheduleAutorun();
732746
}
733747
return currentInstance;
734748
}
749+
750+
private _scheduleAutorun() {
751+
autorunsCreatedCount++;
752+
753+
const next = this._platform.next;
754+
this._autorun = next();
755+
}
735756
}

tests/autorun-test.ts

+39-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ QUnit.test('autorun', function(assert) {
1111
assert.equal(step++, 0);
1212

1313
bb.schedule('zomg', null, () => {
14-
assert.equal(step, 2);
14+
assert.equal(step++, 2);
1515
setTimeout(() => {
1616
assert.ok(!bb.hasTimers(), 'The all timers are cleared');
1717
done();
@@ -58,3 +58,41 @@ QUnit.test('autorun (joins next run if not yet flushed)', function(assert) {
5858
two: { count: 1, order: 1 }
5959
});
6060
});
61+
62+
QUnit.test('autorun completes before items scheduled by later (via microtasks)', function(assert) {
63+
let done = assert.async();
64+
let bb = new Backburner(['first', 'second']);
65+
let order = new Array();
66+
67+
// this later will be scheduled into the `first` queue when
68+
// its timer is up
69+
bb.later(() => {
70+
order.push('second - later');
71+
}, 0);
72+
73+
// scheduling this into the second queue so that we can confirm this _still_
74+
// runs first (due to autorun resolving before scheduled timer)
75+
bb.schedule('second', null, () => {
76+
order.push('first - scheduled');
77+
});
78+
79+
setTimeout(() => {
80+
assert.deepEqual(order, ['first - scheduled', 'second - later']);
81+
done();
82+
}, 20);
83+
});
84+
85+
QUnit.test('can be canceled (private API)', function(assert) {
86+
assert.expect(0);
87+
88+
let done = assert.async();
89+
let bb = new Backburner(['zomg']);
90+
91+
bb.schedule('zomg', null, () => {
92+
assert.notOk(true, 'should not flush');
93+
});
94+
95+
bb['_cancelAutorun']();
96+
97+
setTimeout(done, 10);
98+
});

0 commit comments

Comments
 (0)