From ba72f34017d044041445c9253192ec6e7d71aa2c Mon Sep 17 00:00:00 2001 From: Brian Ford Date: Tue, 6 May 2014 05:33:52 -0700 Subject: [PATCH] feat: expose hooks for enqueuing and dequing tasks --- counting-zone.js | 36 +++++++++---- test/counting-zone.spec.js | 83 +++++++++++++++++++++++++----- zone.js | 100 +++++++++++++++++++++++++++++++++---- 3 files changed, 185 insertions(+), 34 deletions(-) diff --git a/counting-zone.js b/counting-zone.js index 5dd2e5a25..9630caa6c 100644 --- a/counting-zone.js +++ b/counting-zone.js @@ -1,22 +1,36 @@ /* * See example/counting.html */ + Zone.countingZone = { - '-onZoneCreated': function () { - Zone.countingZone.counter += 1; + + // setTimeout + enqueueTask: function () { + this.data.count += 1; }, - '+afterTask': function () { - Zone.countingZone.counter -= 1; - if (Zone.countingZone.counter <= 0) { - Zone.countingZone.counter = 0; - this.onFlush(); - } + + // fires when... + // - clearTimeout + // - setTimeout finishes + dequeueTask: function () { + this.data.count -= 1; }, - '-run': function () { - Zone.countingZone.counter = 0; + + afterTask: function () { + if (this.data.count === 0 && !this.data.flushed) { + this.data.flushed = true; + this.run(this.onFlush); + } }, + counter: function () { - return Zone.countingZone.counter; + return this.data.count; + }, + + data: { + count: 0, + flushed: false }, + onFlush: function () {} }; diff --git a/test/counting-zone.spec.js b/test/counting-zone.spec.js index 77b318683..922daa5fa 100644 --- a/test/counting-zone.spec.js +++ b/test/counting-zone.spec.js @@ -1,33 +1,90 @@ 'use strict'; describe('Zone.countingZone', function () { - var flushSpy = jasmine.createSpy('flush'), - countingZone = zone.fork(Zone.countingZone).fork({ - onFlush: flushSpy - }); + var flushSpy, countingZone; beforeEach(function () { - jasmine.Clock.useMock(); - flushSpy.reset(); + flushSpy = jasmine.createSpy('flush'); + countingZone = zone.fork(Zone.longStackTraceZone). + fork(Zone.countingZone). + fork({ + onFlush: flushSpy + }); }); it('should flush at the end of a run', function () { - countingZone.run(function () {}); - expect(flushSpy).toHaveBeenCalled(); + countingZone.run(function () { + expect(countingZone.counter()).toBe(0); + }); expect(countingZone.counter()).toBe(0); + expect(flushSpy.calls.length).toBe(1); + }); + + it('should work with setTimeout', function () { + var latch; + + runs(function () { + countingZone.run(function () { + setTimeout(function () { + latch = true; + }, 0); + expect(countingZone.counter()).toBe(1); + }); + }); + + waitsFor(function () { + return latch; + }); + + runs(function () { + expect(countingZone.counter()).toBe(0); + }) }); - it('should work', function () { + it('should work with clearTimeout', function () { + var latch = false; countingZone.run(function () { + var id = setTimeout(function () { + latch = true; + }, 0); + expect(countingZone.counter()).toBe(1); + clearTimeout(id); + expect(countingZone.counter()).toBe(0); + }); + }); + - setTimeout(function () {}, 0); + it('should work with addEventListener', function () { + var elt = document.createElement('button'); + var clicked = false; + + runs(function () { + countingZone.run(main); + }); + + function main () { + expect(countingZone.counter()).toBe(0); + elt.addEventListener('click', onClick); expect(countingZone.counter()).toBe(1); - //jasmine.Clock.tick(0); + elt.click(); + function onClick () { + expect(countingZone.counter()).toBe(1); + elt.removeEventListener('click', onClick); + expect(countingZone.counter()).toBe(0); + clicked = true; + } + + expect(countingZone.counter()).toBe(0); + } + + waitsFor(function () { + return clicked; + }, 10, 'the thing'); - //expect(countingZone.counter()).toBe(0); + runs(function () { + expect(flushSpy.calls.length).toBe(1); }); - //jasmine.Clock.tick(0); }); }); diff --git a/zone.js b/zone.js index 1dfcf585a..cb0dff43c 100644 --- a/zone.js +++ b/zone.js @@ -58,12 +58,21 @@ Zone.prototype = { }, bind: function (fn) { + this.enqueueTask(fn); var zone = this.fork(); return function zoneBoundFn() { return zone.run(fn, this, arguments); }; }, + bindOnce: function (fn) { + return this.bind(function () { + var result = fn.apply(this, arguments); + zone.dequeueTask(fn); + return result; + }); + }, + run: function run (fn, applyTo, applyWith) { applyWith = applyWith || []; @@ -90,18 +99,72 @@ Zone.prototype = { beforeTask: function () {}, onZoneCreated: function () {}, - afterTask: function () {} + afterTask: function () {}, + enqueueTask: function () {}, + dequeueTask: function () {} }; -Zone.patchFn = function (obj, fnNames) { + +Zone.patchSetClearFn = function (obj, fnNames) { + fnNames.map(function (name) { + return name[0].toUpperCase() + name.substr(1); + }). + forEach(function (name) { + var setName = 'set' + name; + var clearName = 'clear' + name; + var delegate = obj[setName]; + + if (delegate) { + var ids = {}; + + zone[setName] = function (fn) { + var id; + arguments[0] = function () { + delete ids[id]; + return fn.apply(this, arguments); + }; + var args = Zone.bindArgumentsOnce(arguments); + id = delegate.apply(obj, args); + ids[id] = true; + return id; + }; + + obj[setName] = function () { + return zone[setName].apply(this, arguments); + }; + + var clearDelegate = obj[clearName]; + + zone[clearName] = function (id) { + if (ids[id]) { + delete ids[id]; + zone.dequeueTask(); + } + return clearDelegate.apply(this, arguments); + }; + + obj[clearName] = function () { + return zone[clearName].apply(this, arguments); + }; + } + }); +}; + + +Zone.patchSetFn = function (obj, fnNames) { fnNames.forEach(function (name) { var delegate = obj[name]; + if (delegate) { - zone[name] = function () { - return delegate.apply(obj, Zone.bindArguments(arguments)); + zone[name] = function (fn) { + arguments[0] = function () { + return fn.apply(this, arguments); + }; + var args = Zone.bindArgumentsOnce(arguments); + return delegate.apply(obj, args); }; - obj[name] = function marker () { + obj[name] = function () { return zone[name].apply(this, arguments); }; } @@ -128,6 +191,16 @@ Zone.bindArguments = function (args) { return args; }; + +Zone.bindArgumentsOnce = function (args) { + for (var i = args.length - 1; i >= 0; i--) { + if (typeof args[i] === 'function') { + args[i] = zone.bindOnce(args[i]); + } + } + return args; +}; + Zone.patchableFn = function (obj, fnNames) { fnNames.forEach(function (name) { var delegate = obj[name]; @@ -206,17 +279,24 @@ Zone.patchEventTargetMethods = function (obj) { var removeDelegate = obj.removeEventListener; obj.removeEventListener = function (eventName, fn) { arguments[1] = arguments[1]._bound || arguments[1]; - return removeDelegate.apply(this, arguments); + var result = removeDelegate.apply(this, arguments); + zone.dequeueTask(fn); + return result; }; }; Zone.patch = function patch () { - Zone.patchFn(window, [ - 'setTimeout', - 'setInterval', + Zone.patchSetClearFn(window, [ + 'timeout', + 'interval', + 'immediate' + ]); + + Zone.patchSetFn(window, [ 'requestAnimationFrame', 'webkitRequestAnimationFrame' ]); + Zone.patchableFn(window, ['alert', 'prompt']); // patched properties depend on addEventListener, so this needs to come first @@ -316,7 +396,7 @@ Zone.patchViaCapturingAllTheEvents = function () { }); }; -// TODO: wrap some native API +// wrap some native API on `window` Zone.patchClass = function (className) { var OriginalClass = window[className]; if (!OriginalClass) {