Skip to content

Commit c5a060f

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 angular#8234 Closes angular#9263
1 parent 3dea4a3 commit c5a060f

File tree

4 files changed

+388
-7
lines changed

4 files changed

+388
-7
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

+6-3
Original file line numberDiff line numberDiff line change
@@ -979,7 +979,9 @@ angular.module('ngAnimate', ['ng'])
979979
element = stripCommentsFromElement(element);
980980

981981
if (classBasedAnimationsBlocked(element)) {
982-
return $delegate.setClass(element, add, remove);
982+
if (add) $delegate.$$addClassImmediately(element, add);
983+
if (remove) $delegate.$$removeClassImmediately(element, remove);
984+
return;
983985
}
984986

985987
// we're using a combined array for both the add and remove
@@ -1032,8 +1034,9 @@ angular.module('ngAnimate', ['ng'])
10321034
var classes = resolveElementClasses(element, cache, state.active);
10331035
return !classes
10341036
? done()
1035-
: performAnimation('setClass', classes, element, parentElement, null, function() {
1036-
$delegate.setClass(element, classes[0], classes[1]);
1037+
: performAnimation('setClass', classes, element, null, null, function() {
1038+
if (classes[0]) $delegate.$$addClassImmediately(element, classes[0]);
1039+
if (classes[1]) $delegate.$$removeClassImmediately(element, classes[1]);
10371040
}, done);
10381041
});
10391042
},

0 commit comments

Comments
 (0)