diff options
| author | Matias Niemelä | 2013-10-09 01:12:03 -0400 | 
|---|---|---|
| committer | Misko Hevery | 2013-10-10 17:35:36 -0700 | 
| commit | b1e604e38ceec1714174fb54cc91590a7fe99a92 (patch) | |
| tree | f26d58c2e92a006beede067b0e5a42f09207b598 | |
| parent | 1438f1b62600ab8c4092486a1b4e5e4505565a00 (diff) | |
| download | angular.js-b1e604e38ceec1714174fb54cc91590a7fe99a92.tar.bz2 | |
fix($animate): perform internal caching on getComputedStyle to boost the performance of CSS3 transitions/animations
Closes #4011
Closes #4124
| -rw-r--r-- | src/ngAnimate/animate.js | 105 | ||||
| -rw-r--r-- | test/ngAnimate/animateSpec.js | 53 | 
2 files changed, 114 insertions, 44 deletions
| diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 4bd7b886..f5be6b76 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -642,6 +642,10 @@ angular.module('ngAnimate', ['ng'])            animationIterationCountKey = 'IterationCount',            ELEMENT_NODE = 1; +      var NG_ANIMATE_PARENT_KEY = '$ngAnimateKey'; +      var lookupCache = {}; +      var parentCounter = 0; +        var animationReflowQueue = [], animationTimer, timeOut = false;        function afterReflow(callback) {          animationReflowQueue.push(callback); @@ -652,65 +656,93 @@ angular.module('ngAnimate', ['ng'])            });            animationReflowQueue = [];            animationTimer = null; +          lookupCache = {};          }, 10, false);         } -      function animate(element, className, done) { -        if(['ng-enter','ng-leave','ng-move'].indexOf(className) == -1) { -          var existingDuration = 0; +      function getElementAnimationDetails(element, cacheKey, onlyCheckTransition) { +        var data = lookupCache[cacheKey]; +        if(!data) { +          var transitionDuration = 0, transitionDelay = 0, +              animationDuration = 0, animationDelay = 0; + +          //we want all the styles defined before and after            forEach(element, function(element) {              if (element.nodeType == ELEMENT_NODE) {                var elementStyles = $window.getComputedStyle(element) || {}; -              existingDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), -                                          existingDuration); + +              transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); + +              if(!onlyCheckTransition) { +                transitionDelay  = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); + +                animationDelay   = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); + +                var aDuration  = parseMaxTime(elementStyles[animationProp + durationKey]); + +                if(aDuration > 0) { +                  aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; +                } + +                animationDuration = Math.max(aDuration, animationDuration); +              }              }            }); -          if(existingDuration > 0) { -            done(); -            return; -          } +          data = { +            transitionDelay : transitionDelay, +            animationDelay : animationDelay, +            transitionDuration : transitionDuration, +            animationDuration : animationDuration +          }; +          lookupCache[cacheKey] = data;          } +        return data; +      } -        element.addClass(className); - -        //we want all the styles defined before and after -        var transitionDuration = 0, -            animationDuration = 0, -            transitionDelay = 0, -            animationDelay = 0; -        forEach(element, function(element) { -          if (element.nodeType == ELEMENT_NODE) { -            var elementStyles = $window.getComputedStyle(element) || {}; +      function parseMaxTime(str) { +        var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; +        forEach(values, function(value) { +          total = Math.max(parseFloat(value) || 0, total); +        }); +        return total; +      } -            transitionDelay  = Math.max(parseMaxTime(elementStyles[transitionProp + delayKey]), transitionDelay); +      function getCacheKey(element) { +        var parent = element.parent(); +        var parentID = parent.data(NG_ANIMATE_PARENT_KEY); +        if(!parentID) { +          parent.data(NG_ANIMATE_PARENT_KEY, ++parentCounter); +          parentID = parentCounter; +        } +        return parentID + '-' + element[0].className; +      } -            animationDelay   = Math.max(parseMaxTime(elementStyles[animationProp + delayKey]), animationDelay); +      function animate(element, className, done) { -            transitionDuration = Math.max(parseMaxTime(elementStyles[transitionProp + durationKey]), transitionDuration); +        var cacheKey = getCacheKey(element); +        if(getElementAnimationDetails(element, cacheKey, true).transitionDuration > 0) { -            var aDuration  = parseMaxTime(elementStyles[animationProp + durationKey]); +          done(); +          return; +        } -            if(aDuration > 0) { -              aDuration *= parseInt(elementStyles[animationProp + animationIterationCountKey]) || 1; -            } +        element.addClass(className); -            animationDuration = Math.max(aDuration, animationDuration); -          } -        }); +        var timings = getElementAnimationDetails(element, cacheKey + ' ' + className);          /* there is no point in performing a reflow if the animation             timeout is empty (this would cause a flicker bug normally             in the page. There is also no point in performing an animation             that only has a delay and no duration */ -        var maxDuration = Math.max(transitionDuration, animationDuration); +        var maxDuration = Math.max(timings.transitionDuration, timings.animationDuration);          if(maxDuration > 0) { -          var maxDelayTime = Math.max(transitionDelay, animationDelay) * 1000, +          var maxDelayTime = Math.max(timings.transitionDelay, timings.animationDelay) * 1000,                startTime = Date.now(),                node = element[0];            //temporarily disable the transition so that the enter styles            //don't animate twice (this is here to avoid a bug in Chrome/FF). -          if(transitionDuration > 0) { +          if(timings.transitionDuration > 0) {              node.style[transitionProp + propertyKey] = 'none';            } @@ -723,7 +755,7 @@ angular.module('ngAnimate', ['ng'])            var css3AnimationEvents = animationendEvent + ' ' + transitionendEvent;            afterReflow(function() { -            if(transitionDuration > 0) { +            if(timings.transitionDuration > 0) {                node.style[transitionProp + propertyKey] = '';              }              element.addClass(activeClassName); @@ -768,13 +800,6 @@ angular.module('ngAnimate', ['ng'])            }          } -        function parseMaxTime(str) { -          var total = 0, values = angular.isString(str) ? str.split(/\s*,\s*/) : []; -          forEach(values, function(value) { -            total = Math.max(parseFloat(value) || 0, total); -          }); -          return total; -        }        }        return { diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index 3652e450..cae25266 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -726,10 +726,10 @@ describe("ngAnimate", function() {        it('should re-evaluate the CSS classes for an animation each time',          inject(function($animate, $rootScope, $sniffer, $rootElement, $timeout, $compile) { -        ss.addRule('.abc', '-webkit-transition:22s linear all;' + -                                   'transition:22s linear all;'); -        ss.addRule('.xyz', '-webkit-transition:11s linear all;' + -                                   'transition:11s linear all;'); +        ss.addRule('.abc.ng-enter', '-webkit-transition:22s linear all;' + +                                    'transition:22s linear all;'); +        ss.addRule('.xyz.ng-enter', '-webkit-transition:11s linear all;' + +                                    'transition:11s linear all;');          var parent = $compile('<div><span ng-class="klass"></span></div>')($rootScope);          var element = parent.find('span'); @@ -1875,4 +1875,49 @@ describe("ngAnimate", function() {        expect(intercepted).toBe(true);      });    }); + +  it("should cache the response from getComputedStyle if each successive element has the same className value and parent until the first reflow hits", function() { +    var count = 0; +    module(function($provide) { +      $provide.value('$window', { +        document : jqLite(window.document), +        getComputedStyle: function(element) { +          count++; +          return window.getComputedStyle(element); +        } +      }); +    }); + +    inject(function($animate, $rootScope, $compile, $rootElement, $timeout, $document, $sniffer) { +    if(!$sniffer.transitions) return; + +      $animate.enabled(true); + +      var element = $compile('<div></div>')($rootScope); +      $rootElement.append(element); +      jqLite($document[0].body).append($rootElement); + +      for(var i=0;i<20;i++) { +        var kid = $compile('<div class="kid"></div>')($rootScope); +        $animate.enter(kid, element); +      } +      $rootScope.$digest(); +      $timeout.flush(); + +      expect(count).toBe(2); + +      dealoc(element); +      count = 0; + +      for(var i=0;i<20;i++) { +        var kid = $compile('<div class="kid c-'+i+'"></div>')($rootScope); +        $animate.enter(kid, element); +      } + +      $rootScope.$digest(); +      $timeout.flush(); + +      expect(count).toBe(40); +    }); +  });  }); | 
