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

Commit fa8c399

Browse files
committed
fix(ngAnimate): callback detection should only use RAF when necessary
Callbacks are detected within the internals of ngAnimate whenever an animation starts and ends. In order to allow the user to set callbacks the callback detection needs to happen during the next tick. Prior to this fix we used $$rAF to do the tick detection, however, with this patch we intelligently use $$postDigest to do that for us and then only issue a call to `$$rAF` if necessary.
1 parent 7295c60 commit fa8c399

File tree

3 files changed

+130
-16
lines changed

3 files changed

+130
-16
lines changed

src/ngAnimate/animateQueue.js

+35-9
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,24 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
7575
var disabledElementsLookup = new $$HashMap();
7676
var animationsEnabled = null;
7777

78+
function postDigestTaskFactory() {
79+
var postDigestCalled = false;
80+
return function(fn) {
81+
// we only issue a call to postDigest before
82+
// it has first passed. This prevents any callbacks
83+
// from not firing once the animation has completed
84+
// since it will be out of the digest cycle.
85+
if (postDigestCalled) {
86+
fn();
87+
} else {
88+
$rootScope.$$postDigest(function() {
89+
postDigestCalled = true;
90+
fn();
91+
});
92+
}
93+
};
94+
}
95+
7896
// Wait until all directive and route-related templates are downloaded and
7997
// compiled. The $templateRequest.totalPendingRequests variable keeps track of
8098
// all of the remote templates being currently downloaded. If there are no
@@ -137,14 +155,6 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
137155
return matches;
138156
}
139157

140-
function triggerCallback(event, element, phase, data) {
141-
$$rAF(function() {
142-
forEach(findCallbacks(element, event), function(callback) {
143-
callback(element, phase, data);
144-
});
145-
});
146-
}
147-
148158
return {
149159
on: function(event, container, callback) {
150160
var node = extractElementNode(container);
@@ -239,6 +249,9 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
239249
// These methods will become available after the digest has passed
240250
var runner = new $$AnimateRunner();
241251

252+
// this is used to trigger callbacks in postDigest mode
253+
var runInNextPostDigestOrNow = postDigestTaskFactory();
254+
242255
if (isArray(options.addClass)) {
243256
options.addClass = options.addClass.join(' ');
244257
}
@@ -459,7 +472,20 @@ var $$AnimateQueueProvider = ['$animateProvider', function($animateProvider) {
459472
return runner;
460473

461474
function notifyProgress(runner, event, phase, data) {
462-
triggerCallback(event, element, phase, data);
475+
runInNextPostDigestOrNow(function() {
476+
var callbacks = findCallbacks(element, event);
477+
if (callbacks.length) {
478+
// do not optimize this call here to RAF because
479+
// we don't know how heavy the callback code here will
480+
// be and if this code is buffered then this can
481+
// lead to a performance regression.
482+
$$rAF(function() {
483+
forEach(callbacks, function(callback) {
484+
callback(element, phase, data);
485+
});
486+
});
487+
}
488+
});
463489
runner.progress(event, phase, data);
464490
}
465491

test/ngAnimate/animateSpec.js

+63-7
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,6 @@ describe("animations", function() {
890890
});
891891

892892
$rootScope.$digest();
893-
$animate.flush();
894893
expect(capturedAnimation).toBeTruthy();
895894
expect(runner1done).toBeFalsy();
896895

@@ -910,7 +909,6 @@ describe("animations", function() {
910909
});
911910

912911
$rootScope.$digest();
913-
$animate.flush();
914912
expect(capturedAnimation).toBeTruthy();
915913
expect(runner2done).toBeFalsy();
916914

@@ -990,8 +988,6 @@ describe("animations", function() {
990988
var doneHandler = jasmine.createSpy('addClass done');
991989
runner.done(doneHandler);
992990

993-
$animate.flush();
994-
995991
expect(doneHandler).not.toHaveBeenCalled();
996992

997993
$animate.removeClass(element, 'active-class');
@@ -1011,8 +1007,6 @@ describe("animations", function() {
10111007
var doneHandler = jasmine.createSpy('addClass done');
10121008
runner.done(doneHandler);
10131009

1014-
$animate.flush();
1015-
10161010
expect(doneHandler).not.toHaveBeenCalled();
10171011

10181012
$animate.addClass(element, 'active-class');
@@ -1519,7 +1513,6 @@ describe("animations", function() {
15191513
element = jqLite('<div></div>');
15201514
$animate.enter(element, $rootElement);
15211515
$rootScope.$digest();
1522-
$animate.flush();
15231516

15241517
expect(callbackTriggered).toBe(false);
15251518
}));
@@ -1707,6 +1700,69 @@ describe("animations", function() {
17071700
expect(count).toBe(1);
17081701
}));
17091702

1703+
it('should always detect registered callbacks after one postDigest has fired',
1704+
inject(function($animate, $rootScope, $rootElement) {
1705+
1706+
element = jqLite('<div></div>');
1707+
1708+
var spy = jasmine.createSpy();
1709+
registerCallback();
1710+
1711+
var runner = $animate.enter(element, $rootElement);
1712+
registerCallback();
1713+
1714+
$rootScope.$digest();
1715+
registerCallback();
1716+
1717+
expect(spy.callCount).toBe(0);
1718+
$animate.flush();
1719+
1720+
// this is not 3 since the 3rd callback
1721+
// was added after the first callback
1722+
// was fired
1723+
expect(spy.callCount).toBe(2);
1724+
1725+
spy.reset();
1726+
runner.end();
1727+
1728+
$animate.flush();
1729+
1730+
// now we expect all three callbacks
1731+
// to fire when the animation ends since
1732+
// the callback detection happens again
1733+
expect(spy.callCount).toBe(3);
1734+
1735+
function registerCallback() {
1736+
$animate.on('enter', element, spy);
1737+
}
1738+
}));
1739+
1740+
it('should use RAF if there are detected callbacks within the hierachy of the element being animated',
1741+
inject(function($animate, $rootScope, $rootElement, $$rAF) {
1742+
1743+
var runner;
1744+
1745+
element = jqLite('<div></div>');
1746+
runner = $animate.enter(element, $rootElement);
1747+
$rootScope.$digest();
1748+
runner.end();
1749+
1750+
assertRAFsUsed(false);
1751+
1752+
var spy = jasmine.createSpy();
1753+
$animate.on('leave', element, spy);
1754+
1755+
runner = $animate.leave(element, $rootElement);
1756+
$rootScope.$digest();
1757+
runner.end();
1758+
1759+
assertRAFsUsed(true);
1760+
1761+
function assertRAFsUsed(bool) {
1762+
expect($$rAF.queue.length)[bool ? 'toBeGreaterThan' : 'toBe'](0);
1763+
}
1764+
}));
1765+
17101766
it('leave: should remove the element even if another animation is called after',
17111767
inject(function($animate, $rootScope, $rootElement) {
17121768

test/ngAnimate/integrationSpec.js

+32
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,38 @@ describe('ngAnimate integration tests', function() {
319319
}
320320
});
321321
});
322+
323+
it('should trigger callbacks at the start and end of an animation',
324+
inject(function($rootScope, $rootElement, $animate, $compile) {
325+
326+
ss.addRule('.animate-me', 'transition:2s linear all;');
327+
328+
var parent = jqLite('<div><div ng-if="exp" class="animate-me"></div></div>');
329+
element = parent.find('div');
330+
html(parent);
331+
332+
$compile(parent)($rootScope);
333+
$rootScope.$digest();
334+
335+
var spy = jasmine.createSpy();
336+
$animate.on('enter', parent, spy);
337+
338+
$rootScope.exp = true;
339+
$rootScope.$digest();
340+
341+
element = parent.find('div');
342+
343+
$animate.flush();
344+
345+
expect(spy.callCount).toBe(1);
346+
347+
browserTrigger(element, 'transitionend', { timeStamp: Date.now(), elapsedTime: 2 });
348+
$animate.flush();
349+
350+
expect(spy.callCount).toBe(2);
351+
352+
dealoc(element);
353+
}));
322354
});
323355

324356
describe('JS animations', function() {

0 commit comments

Comments
 (0)