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

Commit 3dd77e0

Browse files
committed
fix(ngAnimate): defer DOM operations for changing classes to postDigest
When ngAnimate is used, it will defer changes to classes until postDigest. Previously, AngularJS (when ngAnimate is not loaded) would always immediately perform these DOM operations. Now, even when the ngAnimate module is not used, if $rootScope is in the midst of a digest, class manipulation is deferred. This helps reduce jank in browsers such as IE11. Closes #8234 Closes #9263
1 parent b6eb3fa commit 3dd77e0

File tree

4 files changed

+387
-6
lines changed

4 files changed

+387
-6
lines changed

src/ng/animate.js

+104-4
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,69 @@ var $AnimateProvider = ['$provide', function($provide) {
8181
return this.$$classNameFilter;
8282
};
8383

84-
this.$get = ['$$q', '$$asyncCallback', function($$q, $$asyncCallback) {
84+
this.$get = ['$$q', '$$asyncCallback', '$rootScope', function($$q, $$asyncCallback, $rootScope) {
8585

8686
var currentDefer;
87+
var ELEMENT_NODE = 1;
88+
89+
function extractElementNodes(element) {
90+
var elements = new Array(element.length);
91+
var count = 0;
92+
for(var i = 0; i < element.length; i++) {
93+
var elm = element[i];
94+
if (elm.nodeType == ELEMENT_NODE) {
95+
elements[count++] = elm;
96+
}
97+
}
98+
elements.length = count;
99+
return jqLite(elements);
100+
}
101+
102+
function runAnimationPostDigest(fn) {
103+
var cancelFn, defer = $$q.defer();
104+
if (!$rootScope.$$phase) {
105+
fn(noop);
106+
defer.resolve();
107+
}
108+
defer.promise.$$cancelFn = function ngAnimateMaybeCancel() {
109+
cancelFn && cancelFn();
110+
};
111+
$rootScope.$$postDigest(function ngAnimatePostDigest() {
112+
cancelFn = fn(function ngAnimateNotifyComplete() {
113+
defer.resolve();
114+
});
115+
});
116+
return defer.promise;
117+
}
118+
119+
function resolveElementClasses(element, cache) {
120+
var map = {};
121+
122+
forEach(cache.add, function(className) {
123+
if (className && className.length) {
124+
map[className] = map[className] || 0;
125+
map[className]++;
126+
}
127+
});
128+
129+
forEach(cache.remove, function(className) {
130+
if (className && className.length) {
131+
map[className] = map[className] || 0;
132+
map[className]--;
133+
}
134+
});
135+
136+
var toAdd = [], toRemove = [];
137+
forEach(map, function(status, className) {
138+
var hasClass = jqLiteHasClass(element[0], className);
139+
140+
if (status < 0 && hasClass) toRemove.push(className);
141+
else if (status > 0 && !hasClass) toAdd.push(className);
142+
});
143+
144+
return (toAdd.length + toRemove.length) > 0 && [toAdd.join(' '), toRemove.join(' ')];
145+
}
146+
87147
function asyncPromise() {
88148
// only serve one instance of a promise in order to save CPU cycles
89149
if (!currentDefer) {
@@ -187,6 +247,11 @@ var $AnimateProvider = ['$provide', function($provide) {
187247
* @return {Promise} the animation callback promise
188248
*/
189249
addClass : function(element, className) {
250+
return this.setClass(element, className, []);
251+
},
252+
253+
$$addClassImmediately : function addClassImmediately(element, className) {
254+
element = jqLite(element);
190255
className = !isString(className)
191256
? (isArray(className) ? className.join(' ') : '')
192257
: className;
@@ -209,6 +274,11 @@ var $AnimateProvider = ['$provide', function($provide) {
209274
* @return {Promise} the animation callback promise
210275
*/
211276
removeClass : function(element, className) {
277+
return this.setClass(element, [], className);
278+
},
279+
280+
$$removeClassImmediately : function removeClassImmediately(element, className) {
281+
element = jqLite(element);
212282
className = !isString(className)
213283
? (isArray(className) ? className.join(' ') : '')
214284
: className;
@@ -232,9 +302,39 @@ var $AnimateProvider = ['$provide', function($provide) {
232302
* @return {Promise} the animation callback promise
233303
*/
234304
setClass : function(element, add, remove) {
235-
this.addClass(element, add);
236-
this.removeClass(element, remove);
237-
return asyncPromise();
305+
var self = this;
306+
var STORAGE_KEY = '$$animateClasses';
307+
element = extractElementNodes(jqLite(element));
308+
309+
add = isArray(add) ? add : add.split(' ');
310+
remove = isArray(remove) ? remove : remove.split(' ');
311+
312+
var cache = element.data(STORAGE_KEY);
313+
if (cache) {
314+
cache.add = cache.add.concat(add);
315+
cache.remove = cache.remove.concat(remove);
316+
//the digest cycle will combine all the animations into one function
317+
return cache.promise;
318+
} else {
319+
element.data(STORAGE_KEY, cache = {
320+
add : add,
321+
remove : remove
322+
});
323+
}
324+
325+
return cache.promise = runAnimationPostDigest(function(done) {
326+
var cache = element.data(STORAGE_KEY);
327+
element.removeData(STORAGE_KEY);
328+
329+
var classes = cache && resolveElementClasses(element, cache);
330+
331+
if (classes) {
332+
if (classes[0].length) self.$$addClassImmediately(element, classes[0]);
333+
if (classes[1].length) self.$$removeClassImmediately(element, classes[1]);
334+
}
335+
336+
done();
337+
});
238338
},
239339

240340
enabled : noop,

src/ngAnimate/animate.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -994,7 +994,9 @@ angular.module('ngAnimate', ['ng'])
994994
element = stripCommentsFromElement(element);
995995

996996
if (classBasedAnimationsBlocked(element)) {
997-
return $delegate.setClass(element, add, remove);
997+
if (add) $delegate.$$addClassImmediately(element, add);
998+
if (remove) $delegate.$$removeClassImmediately(element, remove);
999+
return;
9981000
}
9991001

10001002
add = isArray(add) ? add : add.split(' ');
@@ -1023,7 +1025,8 @@ angular.module('ngAnimate', ['ng'])
10231025
return !classes
10241026
? done()
10251027
: performAnimation('setClass', classes, element, null, null, function() {
1026-
$delegate.setClass(element, classes[0], classes[1]);
1028+
if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
1029+
if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
10271030
}, done);
10281031
});
10291032
},

0 commit comments

Comments
 (0)