• AngularJS渲染性能分析


    作者:Jiang, Jilin


    AngularJS中,通过数据绑定。能够十分方便的构建页面。可是当面对复杂的循环嵌套结构时,渲染会遇到性能瓶颈。今天,我们将通过一些列实验,来測试AngularJS的渲染性能,对照ng-show。ng-if的使用场景。并对优化进行简要分析。

     

    只是在此之前,我们须要先简单过一遍AngularJS相关的代码:

    $apply: function(expr) {
      try {
        beginPhase('$apply');
        try {
          return this.$eval(expr);
        } finally {
          clearPhase();
        }
      } catch (e) {
        $exceptionHandler(e);
      } finally {
        try {
          $rootScope.$digest();
        } catch (e) {
          $exceptionHandler(e);
          throw e;
        }
      }
    },
    

    beginPhase和clearPhase用于对$rootScope.$$phase进行锁定。假设发现反复进入$apply阶段则抛出异常。以免出现死循环。

    $eval: function(expr, locals) {
      return $parse(expr)(this, locals);
    },
    


     $parse调用的是$ParseProvider。

    因为之后的实验expr不传值。所以$ParseProvider会直接返回空函数noop() {}。

    因此我们就不做详细的$ParseProvider内容分析了。



    在运行完$eval后。会调用$digest方法。

    让我们看看$digest里有些什么:

    $digest: function() {
      var watch, value, last,
          watchers,
          length,
          dirty, ttl = TTL,
          next, current, target = this,
          watchLog = [],
          logIdx, logMsg, asyncTask;
    
      beginPhase('$digest');
      // Check for changes to browser url that happened in sync before the call to $digest
      $browser.$$checkUrlChange();
    
      if (this === $rootScope && applyAsyncId !== null) {
        // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
        // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
        $browser.defer.cancel(applyAsyncId);
        flushApplyAsync();
      }
    
      lastDirtyWatch = null;
    
      do { // "while dirty" loop
        dirty = false;
        current = target;
    
        while (asyncQueue.length) {
          try {
            asyncTask = asyncQueue.shift();
            asyncTask.scope.$eval(asyncTask.expression, asyncTask.locals);
          } catch (e) {
            $exceptionHandler(e);
          }
          lastDirtyWatch = null;
        }
    
        traverseScopesLoop:
        do { // "traverse the scopes" loop
          if ((watchers = current.$$watchers)) {
            // process our watches
            length = watchers.length;
            while (length--) {
              try {
                watch = watchers[length];
                // Most common watches are on primitives, in which case we can short
                // circuit it with === operator, only when === fails do we use .equals
                if (watch) {
                  if ((value = watch.get(current)) !== (last = watch.last) &&
                      !(watch.eq
                          ?

    equals(value, last) : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; watchLog[logIdx].push({ msg: isFunction(watch.exp) ?

    'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, newVal: value, oldVal: last }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { $exceptionHandler(e); } } } // Insanity Warning: scope depth-first traversal // yes, this code is a bit crazy, but it works and we have tests to prove it! // this piece should be kept in sync with the traversal in $broadcast if (!(next = ((current.$$watchersCount && current.$$childHead) || (current !== target && current.$$nextSibling)))) { while (current !== target && !(next = current.$$nextSibling)) { current = current.$parent; } } } while ((current = next)); // `break traverseScopesLoop;` takes us to here if ((dirty || asyncQueue.length) && !(ttl--)) { clearPhase(); throw $rootScopeMinErr('infdig', '{0} $digest() iterations reached. Aborting! ' + 'Watchers fired in the last 5 iterations: {1}', TTL, watchLog); } } while (dirty || asyncQueue.length); clearPhase(); while (postDigestQueue.length) { try { postDigestQueue.shift()(); } catch (e) { $exceptionHandler(e); } } },


    相同的,调用beginPhase改变阶段。



    $browser.$$checkUrlChange()用于检測url是否变更。这次我们也用不到:

    function fireUrlChange() {
      if (lastBrowserUrl === self.url() && lastHistoryState === cachedState) {
        return;
      }
    
      lastBrowserUrl = self.url();
      lastHistoryState = cachedState;
      forEach(urlChangeListeners, function(listener) {
        listener(self.url(), cachedState);
      });
    }
    

    接着进行$rootScope和applyAsyncId推断。假设是根Scope而且存在异步apply请求。则调用$eval并把队列清空。也不是本次须要用到的部分。

     

    进入循环,asyncQueue保存了$evalAsync方法的数据。

    用不到。

     

    之后设置了一个断点,用于跳出内部循环:

    traverseScopesLoop:

    循环内推断是否存在$$watchers列表,然后对watch单元进行变更匹配。每一个页面的数据绑定都会相应到一个watch单元。此处会检查是否watch是深匹配,假设为真会调用equals方法进行递归检查,假设watch了一个巨大的对象。那么equals会十分消耗性能。反之,则会检查是否是NaN,js中NaN != NaN。然而假设原值和现值都是NaN,事实上是没有变更过的。

    if (watch) {
      if ((value = watch.get(current)) !== (last = watch.last) &&
          !(watch.eq
              ? equals(value, last)
              : (typeof value === 'number' && typeof last === 'number'
                 && isNaN(value) && isNaN(last)))) {
    

    假设循环后已经发现watch单元原值和现值相等,会跳出循环。

    再次又一次验证,目的是为了防止某个watch调用回调函数后。使得之前的watch现值发生变化。

    而当中也设置了ttl循环计数。以免出现watch不断改变产生死循环的问题。



    接着,就是著名的crazy凝视了:

    // Insanity Warning: scope depth-first traversal
    // yes, this code is a bit crazy, but it works and we have tests to prove it!
    // this piece should be kept in sync with the traversal in $broadcast
    

    此处会深度优先遍历,然后反复上面的检查。直到遍历结束。

    作者非常贴心的标注一下循环结束了:

    // `break traverseScopesLoop;` takes us to here


    后面的代码就十分好懂了,clearPhase。然后处理DigestQueue结束循环。

     

    之后检查ttl数值,假设ttl值超出了10次(预设值),则会抛出过多循环的异常。

     

    实验

    简单的过了一遍代码后。我们開始做一下性能測试:(注:因为不同机器配置性能不同,渲染时间仅作横向对照之用)

    如今。如果我们拥有2个用户组。每组用户拥有1000个用户信息。用户信息例如以下:

    [{name: "user1"}, {name:"user2"},...]

     

    我们第一步做最简单未经过优化的渲染:

    <div>
       <div ng-repeat="user in userList">
          <label>Name</label>
          <p>{{user.name}}</p>
       </div>
    </div>
    

    切换分组渲染时间平均310ms左右。

     

    track by

    然后简单使用优化track by优化:

    ng-repeat="user in userList track by $index"

    第一次渲染260ms左右。之后切换耗费11ms左右。

     

    效果不错。接着,我们比較不同长度的数组切换比較。如果用户组1长度仍然为1000,用户组2长度100:(下图中,状态1、2代表绑定数组的切换)

    状态1状态2

    用户组1

    用户组2

    用户组1

    ~0.3ms

    ~111ms

    用户组2

    ~175ms

    ~0.1ms

    我们能够看出,元素动态创建/删除会极大影响渲染性能。

    创建相同数量元素比删除相同数量元素更消耗性能。

     

    ng-show

    基于以上实验。我们能够非常easy想到。假设我们使用元素池,预先创建足量的元素。接着通过ng-show来动态调整显示的元素。这样性能是否会上升呢?

     

    $scope.getTimes = function(n) {
    return new Array(n);
    };
    <div ng-repeat="i in getTimes(1000) track by $index" ng-show="userList[$index]">
       <label>Name</label>
       <p>{{userList[$index].name}}</p>
    </div>
    

    状态1状态2

    用户组1

    用户组2

    用户组1

    ~1.3ms

    ~42ms

    用户组2

    ~22ms

    ~1.0ms



    能够发现。同组切换时间消耗少量添加。

    可是相对的,异组切换性能大幅提升了。

    这是因为web中,元素操作是十分消耗性能的操作。因而为了性能。我们须要尽可能避免元素的创建/删除。相同的,因为每次渲染,都会调用new Array和检查ng-show属性,从而导致了同组切换的时间添加了。




    ng-if与ng-show

    Angularjs中还有还有一个方法ng-if,它是仅仅有满足表达式条件才会变更元素。对于用户组切换,其毫无疑问会创建/删除元素。只是在此,我还是把数据罗列一下:

    <div ng-repeat="i in getTimes(1000) track by $index" ng-if="userList[$index]">
       <label>Name</label>
       <p>{{userList[$index].name}}</p>
    </div>
    

    状态1状态2

    用户组1

    用户组2

    用户组1

    ~11ms

    ~250ms

    用户组2

    ~300ms

    ~5.5ms


    能够看出,使用缓存+ng-if。性能消耗会比原本没有track by更消耗性能。


    那么ng-if的适用场景是什么?是否全部的ng-if都适合被ng-show取代呢?让我们接下去继续看看列子。

     

    组合

    首先。我们对照一下有无缓存的初始化1000条数据的时间。

    有缓存

    无缓存

    用户组1

    ~276ms

    ~240ms

    用户组2

    ~278ms

    ~36ms

     

    如今,我们如果用户有一个id属性。UI中,依据id是除以5的余数来做不同的渲染。规则例如以下:

    余数

    渲染元素

    0

    画一个2*2的table

    1

    显示一个长度为5的ul li列表

    2

    显示一个checkbox的input

    3

    显示一个textarea

    4

    显示一个text input

    你可能已经看出我的想法了,我们的目的在于測试。假设存在多个不同渲染方式的情况下,是否适合使用ng-show。我们来看一下,(ng-switch近似ng-if,我们一起增加对照)

    <div ng-repeat="user in userList track by $index">
       <label>Name</label>
       <p>{{user.name}}</p>
    
       <div ng-show="user.id % 5 === 0">
          <table>
             <tbody>
                <tr>
                   <th>11</th>
                   <th>12</th>
                </tr>
                <tr>
                   <th>21</th>
                   <th>22</th>
                </tr>
             </tbody>
          </table>
       </div>
    
       <div ng-show="user.id % 5 === 1">
          <ul>
             <li>1</li>
             <li>2</li>
             <li>3</li>
             <li>4</li>
             <li>5</li>
          </ul>
       </div>
    
       <div ng-show="user.id % 5 === 2">
          <input type="checkbox" />
       </div>
    
       <div ng-show="user.id % 5 === 3">
          <textarea></textarea>
       </div>
    
       <div ng-show="user.id % 5 === 4">
          <input type="text" />
       </div>
    </div>
    <div ng-repeat="user in userList track by $index">
       <label>Name</label>
       <p>{{user.name}}</p>
    
       <div ng-if="user.id % 5 === 0">
          <table>
             <tbody>
                <tr>
                   <th>11</th>
                   <th>12</th>
                </tr>
                <tr>
                   <th>21</th>
                   <th>22</th>
                </tr>
             </tbody>
          </table>
       </div>
    
       <div ng-if="user.id % 5 === 1">
          <ul>
             <li>1</li>
             <li>2</li>
             <li>3</li>
             <li>4</li>
             <li>5</li>
          </ul>
       </div>
    
       <div ng-if="user.id % 5 === 2">
          <input type="checkbox" />
       </div>
    
       <div ng-if="user.id % 5 === 3">
          <textarea></textarea>
       </div>
    
       <div ng-if="user.id % 5 === 4">
          <input type="text" />
       </div>
    </div>
    


     

    ng-show

    ng-if

    ng-switch

    用户组1

    ~557ms

    ~766ms

    ~858ms



    接着,測试切换:

    ng-show

    ng-if

    ng-switch

    组1->组2

    ~260ms

    ~257ms

    ~261ms

    组2->组1

    ~430ms

    ~470ms

    ~560ms

     好像ng-show各项数值都优于ng-if与ng-switch。只是还没完,我们继续改动样例。

    为用户加入下面几个属性,相应绑定于之前定义的元素(m,n初始化时伪随机生成以便于測试对照数值):

    属性

    描写叙述

    matrix

    一个m*n的数组

    list

    一个长度为n的列表

    desc

    string

    checked

    boolean


    <div ng-repeat="user in userList track by $index">
       <label>Name</label>
       <p>{{user.name}}</p>
    
       <div ng-show="user.id % 5 === 0">
          <table>
             <tbody>
                <tr ng-repeat="line in user.matrix track by $index">
                   <th ng-repeat="val in line track by $index">{{val}}</th>
                </tr>
             </tbody>
          </table>
       </div>
    
       <div ng-show="user.id % 5 === 1">
          <ul>
             <li ng-repeat="val in user.list track by $index">{{val}}</li>
          </ul>
       </div>
    
       <div ng-show="user.id % 5 === 2">
          <input type="checkbox" ng-checked="user.checked" />
       </div>
    
       <div ng-show="user.id % 5 === 3">
          <textarea ng-model="user.desc"></textarea>
       </div>
    
       <div ng-show="user.id % 5 === 4">
          <input type="text" ng-model="user.desc" />
       </div>
    </div>
    <div ng-repeat="user in userList track by $index">
       <label>Name</label>
       <p>{{user.name}}</p>
    
       <div ng-if="user.id % 5 === 0">
          <table>
             <tbody>
                <tr ng-repeat="line in user.matrix track by $index">
                   <th ng-repeat="val in line track by $index">{{val}}</th>
                </tr>
             </tbody>
          </table>
       </div>
    
       <div ng-if="user.id % 5 === 1">
          <ul>
             <li ng-repeat="val in user.list track by $index">{{val}}</li>
          </ul>
       </div>
    
       <div ng-if="user.id % 5 === 2">
          <input type="checkbox" ng-checked="user.checked" />
       </div>
    
       <div ng-if="user.id % 5 === 3">
          <textarea ng-model="user.desc"></textarea>
       </div>
    
       <div ng-if="user.id % 5 === 4">
          <input type="text" ng-model="user.desc" />
       </div>
    </div>
    

     

    ng-show

    ng-if

    ng-switch

    用户组1

    ~4678ms

    ~1800ms

    ~1990ms


    是不是大吃一惊?原因非常easy,因为ng-show仅仅是隐藏元素。

    可是实际的数据绑定仍旧会被运行。

    尽管在页面上看不到,可是元素绑定的数据还是一并更改了:



    通过以上实验,我们非常easy分析出。当页面布局简单时,能够通过ng-show+cachelist来实现高速的数据切换。而当元素组件存在大量元素变化的时候,使用ng-if/ng-switch来避免多余的元素绑定。

    通过两者结合的方式,能够使得程序在初始化和动态变化的时候保持更好的性能。相同的,在事件处理中。ng-if相较于ng-show会更有利于性能,可是假设事件绑定不多,使用ng-show则更佳。






  • 相关阅读:
    Ryzom简易汉化教程
    在Windows上编译运行Ryzom客户端
    在Windows(x86)上编译、配置并运行Ryzom Core(服务器/客户端)
    引擎设计与商业模式
    总结了一下新手学习Windows 8 Metro App 开发的捷径
    开始研究Ryzom Core!
    和Ryzom相关的项目简介
    关于Ryzom游戏开发的路线图
    根据 yyyymmdd格式日期取得当前日期所在周的开始和结束日期
    asp数组中REDIM的用法(动态数组)
  • 原文地址:https://www.cnblogs.com/claireyuancy/p/6955102.html
Copyright © 2020-2023  润新知