Skip to content

Commit 32d48cf

Browse files
committed
perf($animate): listen for document visibility changes
perf(ngAnimate): listen for document visibility changes Accessing the document for the hidden state is costly for platforms like Electron. Instead, listen for visibilitychange and store the state. (angular#14071) Closes angular#14066
1 parent a2a907e commit 32d48cf

File tree

8 files changed

+99
-50
lines changed

8 files changed

+99
-50
lines changed

src/AngularPublic.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
$ControllerProvider,
6666
$DateProvider,
6767
$DocumentProvider,
68+
$$IsDocumentHiddenProvider,
6869
$ExceptionHandlerProvider,
6970
$FilterProvider,
7071
$$ForceReflowProvider,
@@ -227,6 +228,7 @@ function publishExternalAPI(angular) {
227228
$cacheFactory: $CacheFactoryProvider,
228229
$controller: $ControllerProvider,
229230
$document: $DocumentProvider,
231+
$$isDocumentHidden: $$IsDocumentHiddenProvider,
230232
$exceptionHandler: $ExceptionHandlerProvider,
231233
$filter: $FilterProvider,
232234
$$forceReflow: $$ForceReflowProvider,

src/ng/animateRunner.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ var $$AnimateAsyncRunFactoryProvider = function() {
2828
};
2929

3030
var $$AnimateRunnerFactoryProvider = function() {
31-
this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$document', '$timeout',
32-
function($q, $sniffer, $$animateAsyncRun, $document, $timeout) {
31+
this.$get = ['$q', '$sniffer', '$$animateAsyncRun', '$$isDocumentHidden', '$timeout',
32+
function($q, $sniffer, $$animateAsyncRun, $$isDocumentHidden, $timeout) {
3333

3434
var INITIAL_STATE = 0;
3535
var DONE_PENDING_STATE = 1;
@@ -81,11 +81,7 @@ var $$AnimateRunnerFactoryProvider = function() {
8181

8282
this._doneCallbacks = [];
8383
this._tick = function(fn) {
84-
var doc = $document[0];
85-
86-
// the document may not be ready or attached
87-
// to the module for some internal tests
88-
if (doc && doc.hidden) {
84+
if ($$isDocumentHidden()) {
8985
timeoutTick(fn);
9086
} else {
9187
rafTick(fn);

src/ng/document.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,29 @@ function $DocumentProvider() {
3030
return jqLite(window.document);
3131
}];
3232
}
33+
34+
35+
/**
36+
* @private
37+
* Listens for document visibility change and makes the current status accessible.
38+
*/
39+
function $$IsDocumentHiddenProvider() {
40+
this.$get = ['$document', '$rootScope', function($document, $rootScope) {
41+
var doc = $document[0];
42+
var hidden = doc && doc.hidden;
43+
44+
$document.on('visibilitychange', changeListener);
45+
46+
$rootScope.$on('$destroy', function() {
47+
$document.off('visibilitychange', changeListener);
48+
});
49+
50+
function changeListener() {
51+
hidden = doc.hidden;
52+
}
53+
54+
return function() {
55+
return hidden;
56+
};
57+
}];
58+
}

src/ngAnimate/animateQueue.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,10 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
102102

103103
this.$get = ['$$rAF', '$rootScope', '$rootElement', '$document', '$$HashMap',
104104
'$$animation', '$$AnimateRunner', '$templateRequest', '$$jqLite', '$$forceReflow',
105+
'$$isDocumentHidden',
105106
function($$rAF, $rootScope, $rootElement, $document, $$HashMap,
106-
$$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow) {
107+
$$animation, $$AnimateRunner, $templateRequest, $$jqLite, $$forceReflow,
108+
$$isDocumentHidden) {
107109

108110
var activeAnimationsLookup = new $$HashMap();
109111
var disabledElementsLookup = new $$HashMap();
@@ -367,7 +369,7 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
367369

368370
var isStructural = ['enter', 'move', 'leave'].indexOf(event) >= 0;
369371

370-
var documentHidden = $document[0].hidden;
372+
var documentHidden = $$isDocumentHidden();
371373

372374
// this is a hard disable of all animations for the application or on
373375
// the element itself, therefore there is no need to continue further

test/ng/animateRunnerSpec.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,13 @@ describe("$$AnimateRunner", function() {
163163
}));
164164

165165
it("should use timeouts to trigger async operations when the document is hidden", function() {
166-
var doc;
166+
var hidden = true;
167167

168168
module(function($provide) {
169-
doc = jqLite({
170-
body: document.body,
171-
hidden: true
169+
170+
$provide.value('$$isDocumentHidden', function() {
171+
return hidden;
172172
});
173-
$provide.value('$document', doc);
174173
});
175174

176175
inject(function($$AnimateRunner, $rootScope, $$rAF, $timeout) {
@@ -184,7 +183,7 @@ describe("$$AnimateRunner", function() {
184183
$timeout.flush();
185184
expect(spy).toHaveBeenCalled();
186185

187-
doc[0].hidden = false;
186+
hidden = false;
188187

189188
spy = jasmine.createSpy();
190189
runner = new $$AnimateRunner();

test/ng/compileSpec.js

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7213,22 +7213,22 @@ describe('$compile', function() {
72137213
});
72147214

72157215
inject(function($compile, $rootScope) {
7216-
expect(jqLiteCacheSize()).toEqual(0);
7216+
var cacheSize = jqLiteCacheSize();
72177217

72187218
element = $compile('<div><div ng-repeat="x in xs" ng-if="x==1">{{x}}</div></div>')($rootScope);
7219-
expect(jqLiteCacheSize()).toEqual(1);
7219+
expect(jqLiteCacheSize()).toEqual(cacheSize + 1);
72207220

72217221
$rootScope.$apply('xs = [0,1]');
7222-
expect(jqLiteCacheSize()).toEqual(2);
7222+
expect(jqLiteCacheSize()).toEqual(cacheSize + 2);
72237223

72247224
$rootScope.$apply('xs = [0]');
7225-
expect(jqLiteCacheSize()).toEqual(1);
7225+
expect(jqLiteCacheSize()).toEqual(cacheSize + 1);
72267226

72277227
$rootScope.$apply('xs = []');
7228-
expect(jqLiteCacheSize()).toEqual(1);
7228+
expect(jqLiteCacheSize()).toEqual(cacheSize + 1);
72297229

72307230
element.remove();
7231-
expect(jqLiteCacheSize()).toEqual(0);
7231+
expect(jqLiteCacheSize()).toEqual(cacheSize + 0);
72327232
});
72337233
});
72347234

@@ -7245,22 +7245,22 @@ describe('$compile', function() {
72457245
});
72467246

72477247
inject(function($compile, $rootScope) {
7248-
expect(jqLiteCacheSize()).toEqual(0);
7248+
var cacheSize = jqLiteCacheSize();
72497249

72507250
element = $compile('<div><div ng-repeat="x in xs" ng-if="x==1">{{x}}</div></div>')($rootScope);
7251-
expect(jqLiteCacheSize()).toEqual(0);
7251+
expect(jqLiteCacheSize()).toEqual(cacheSize);
72527252

72537253
$rootScope.$apply('xs = [0,1]');
7254-
expect(jqLiteCacheSize()).toEqual(0);
7254+
expect(jqLiteCacheSize()).toEqual(cacheSize);
72557255

72567256
$rootScope.$apply('xs = [0]');
7257-
expect(jqLiteCacheSize()).toEqual(0);
7257+
expect(jqLiteCacheSize()).toEqual(cacheSize);
72587258

72597259
$rootScope.$apply('xs = []');
7260-
expect(jqLiteCacheSize()).toEqual(0);
7260+
expect(jqLiteCacheSize()).toEqual(cacheSize);
72617261

72627262
element.remove();
7263-
expect(jqLiteCacheSize()).toEqual(0);
7263+
expect(jqLiteCacheSize()).toEqual(cacheSize);
72647264
});
72657265
});
72667266

@@ -7276,26 +7276,26 @@ describe('$compile', function() {
72767276
});
72777277

72787278
inject(function($compile, $rootScope) {
7279-
expect(jqLiteCacheSize()).toEqual(0);
7279+
var cacheSize = jqLiteCacheSize();
72807280
element = $compile('<div><div ng-repeat="x in xs" ng-if="val">{{x}}</div></div>')($rootScope);
72817281

72827282
$rootScope.$apply('xs = [0,1]');
72837283
// At this point we have a bunch of comment placeholders but no real transcluded elements
72847284
// So the cache only contains the root element's data
7285-
expect(jqLiteCacheSize()).toEqual(1);
7285+
expect(jqLiteCacheSize()).toEqual(cacheSize + 1);
72867286

72877287
$rootScope.$apply('val = true');
72887288
// Now we have two concrete transcluded elements plus some comments so two more cache items
7289-
expect(jqLiteCacheSize()).toEqual(3);
7289+
expect(jqLiteCacheSize()).toEqual(cacheSize + 3);
72907290

72917291
$rootScope.$apply('val = false');
72927292
// Once again we only have comments so no transcluded elements and the cache is back to just
72937293
// the root element
7294-
expect(jqLiteCacheSize()).toEqual(1);
7294+
expect(jqLiteCacheSize()).toEqual(cacheSize + 1);
72957295

72967296
element.remove();
72977297
// Now we've even removed the root element along with its cache
7298-
expect(jqLiteCacheSize()).toEqual(0);
7298+
expect(jqLiteCacheSize()).toEqual(cacheSize + 0);
72997299
});
73007300
});
73017301

@@ -7332,6 +7332,7 @@ describe('$compile', function() {
73327332
});
73337333

73347334
inject(function($compile, $rootScope, $httpBackend, $timeout, $templateCache) {
7335+
var cacheSize = jqLiteCacheSize();
73357336
$httpBackend.whenGET('red.html').respond('<p>red.html</p>');
73367337
var template = $compile(
73377338
'<div ng-controller="Leak">' +
@@ -7346,7 +7347,7 @@ describe('$compile', function() {
73467347
$timeout.flush();
73477348
$httpBackend.flush();
73487349
expect(linkFn).not.toHaveBeenCalled();
7349-
expect(jqLiteCacheSize()).toEqual(2);
7350+
expect(jqLiteCacheSize()).toEqual(cacheSize + 2);
73507351

73517352
$templateCache.removeAll();
73527353
var destroyedScope = $rootScope.$new();
@@ -8129,9 +8130,7 @@ describe('$compile', function() {
81298130

81308131
it('should not leak memory with nested transclusion', function() {
81318132
inject(function($compile, $rootScope) {
8132-
var size;
8133-
8134-
expect(jqLiteCacheSize()).toEqual(0);
8133+
var size, initialSize = jqLiteCacheSize();
81358134

81368135
element = jqLite('<div><ul><li ng-repeat="n in nums">{{n}} => <i ng-if="0 === n%2">Even</i><i ng-if="1 === n%2">Odd</i></li></ul></div>');
81378136
$compile(element)($rootScope.$new());
@@ -8145,7 +8144,7 @@ describe('$compile', function() {
81458144
expect(jqLiteCacheSize()).toEqual(size);
81468145

81478146
element.remove();
8148-
expect(jqLiteCacheSize()).toEqual(0);
8147+
expect(jqLiteCacheSize()).toEqual(initialSize);
81498148
});
81508149
});
81518150
});

test/ng/documentSpec.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,29 @@ describe('$document', function() {
2727
});
2828
});
2929
});
30+
31+
32+
describe('$$isDocumentHidden', function() {
33+
it('should listen on the visibilitychange event', function() {
34+
var doc;
35+
36+
var spy = spyOn(document, 'addEventListener').and.callThrough();
37+
38+
inject(function($$isDocumentHidden, $document) {
39+
expect(spy.calls.mostRecent().args[0]).toBe('visibilitychange');
40+
expect(spy.calls.mostRecent().args[1]).toEqual(jasmine.any(Function));
41+
expect($$isDocumentHidden()).toBeFalsy(); // undefined in browsers that don't support visibility
42+
});
43+
44+
});
45+
46+
it('should remove the listener when the $rootScope is destroyed', function() {
47+
var spy = spyOn(document, 'removeEventListener').and.callThrough();
48+
49+
inject(function($$isDocumentHidden, $rootScope) {
50+
$rootScope.$destroy();
51+
expect(spy.calls.mostRecent().args[0]).toBe('visibilitychange');
52+
expect(spy.calls.mostRecent().args[1]).toEqual(jasmine.any(Function));
53+
});
54+
});
55+
});

test/ngAnimate/animateSpec.js

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -157,14 +157,12 @@ describe("animations", function() {
157157
}));
158158

159159
it("should skip animations entirely if the document is hidden", function() {
160-
var doc;
160+
var hidden = true;
161161

162162
module(function($provide) {
163-
doc = jqLite({
164-
body: document.body,
165-
hidden: true
163+
$provide.value('$$isDocumentHidden', function() {
164+
return hidden;
166165
});
167-
$provide.value('$document', doc);
168166
});
169167

170168
inject(function($animate, $rootScope) {
@@ -173,7 +171,7 @@ describe("animations", function() {
173171
expect(capturedAnimation).toBeFalsy();
174172
expect(element[0].parentNode).toEqual(parent[0]);
175173

176-
doc[0].hidden = false;
174+
hidden = false;
177175

178176
$animate.leave(element);
179177
$rootScope.$digest();
@@ -2503,18 +2501,19 @@ describe("animations", function() {
25032501

25042502

25052503
describe('because the document is hidden', function() {
2506-
beforeEach(module(function($provide) {
2507-
var doc = jqLite({
2508-
body: document.body,
2509-
hidden: true
2504+
var hidden = true;
2505+
2506+
beforeEach(function() {
2507+
module(function($provide) {
2508+
$provide.value('$$isDocumentHidden', function() {
2509+
return hidden;
2510+
});
25102511
});
2511-
$provide.value('$document', doc);
2512-
}));
2512+
});
25132513

25142514
it('should trigger callbacks for an enter animation',
25152515
inject(function($animate, $rootScope, $rootElement, $document) {
25162516

2517-
var callbackTriggered = false;
25182517
var spy = jasmine.createSpy();
25192518
$animate.on('enter', jqLite($document[0].body), spy);
25202519

0 commit comments

Comments
 (0)