作者: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!\n' +
'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则更佳。

AngularJS渲染性能分析的更多相关文章

  1. 聚焦性能技术和实践, MTSC全面揭秘PerfDog演进之路

    商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处. 12月14日,2019年度中国移动互联网测试开发大会(Mobile Testing Summit China,简称 MTSC)深圳站于深 ...

  2. AngularJS 开发中常犯的10个错误

    简介 AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS非常善于构建小型app原型,但AngularJS对于全功能的客 ...

  3. AngularJS开发人员最常犯的10个错误

    简介AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS非常善于构建小型app原型,但AngularJS对于全功能的客户 ...

  4. AngularJS开发最常犯的10个错误

    简介 AngularJS是目前最为活跃的Javascript框架之一,AngularJS的目标之一是简化开发过程,这使得AngularJS非常善于构建小型app原型,但AngularJS对于全功能的客 ...

  5. 通过AngularJS实现前端与后台的数据对接(二)——服务(service,$http)篇

    什么是服务? 服务提供了一种能在应用的整个生命周期内保持数据的方法,它能够在控制器之间进行通信,并且能保证数据的一致性. 服务是一个单例对象,在每个应用中只会被实例化一次(被$injector实例化) ...

  6. AngularJs之九(ending......)

    今天继续angularJs,但也是最后一篇关于它的了,基础部分差不多也就这些,后续有机会再写它的提升部分. 今天要写的也是一个基础的选择列表: 一:使用ng-options,数组进行循环. <d ...

  7. AngularJS过滤器filter-保留小数,小数点-$filter

    AngularJS      保留小数 默认是保留3位 固定的套路是 {{deom | number:4}} 意思就是保留小数点 的后四位 在渲染页面的时候 加入这儿个代码 用来精确浮点数,指定小数点 ...

  8. Angular企业级开发(1)-AngularJS简介

    AngularJS介绍 AngularJS是一个功能完善的JavaScript前端框架,同时是基于MVC(Model-View-Controller理念的框架,使用它能够高效的开发桌面web app和 ...

  9. 模拟AngularJS之依赖注入

    一.概述 AngularJS有一经典之处就是依赖注入,对于什么是依赖注入,熟悉spring的同学应该都非常了解了,但,对于前端而言,还是比较新颖的. 依赖注入,简而言之,就是解除硬编码,达到解偶的目的 ...

随机推荐

  1. mpstat---用于多CPU环境下,显示各个可用CPU的状态

    mpstat命令指令主要用于多CPU环境下,它显示各个可用CPU的状态系你想.这些信息存放在/proc/stat文件中.在多CPUs系统里,其不但能查看所有CPU的平均状况信息,而且能够查看特定CPU ...

  2. caioj 1106 树形动态规划(TreeDP)1:加分二叉树

    解这道题的前提是非常熟悉中序遍历的方式 我就是因为不熟悉而没有做出来 中序遍历是5 7 1 2 10的话,如果1是根节点 那么5 7 1就是1的左子树,2, 10就是右子树 这就有点中链式dp的味道了 ...

  3. 【SICP练习】152 练习4.8

    练习4-8 原文 Exercise 4.8. "Named let" is a variant of let that has the form (let <var> ...

  4. HIT 2255 Not Fibonacci(矩阵高速幂)

    #include <iostream> #include <cstdio> #include <cstring> using namespace std; cons ...

  5. POJ 1895 分层图网络流+输出路径

    题意: 题目描述:在公元3141年人类的足迹已经遍布银河系.为了穿越那巨大的距离,人类发明了一种名为超时空轨道的技术.超时空轨道是双向的,连接两个星系,穿越轨道需要一天的时间.然而这个轨道只能同时给一 ...

  6. Ionic2集成ngx-datatable,ng2-tree第三方控件.md

    1. 基本环境配置 1.1. 命令安装相应的依赖 1.2. 在Module定义中引入对应Module 1.3. 引入对应的CSS 2. 简单使用示例验证是否集成成功 2.1. ngx-datatabl ...

  7. .Net Web开发技术栈 收藏

    原文:http://www.cnblogs.com/1996V/p/7700087.html#!comments 有很多朋友有的因为兴趣,有的因为生计而走向了.Net中,有很多朋友想学,但是又不知道怎 ...

  8. 实现图片懒加载(lazyload)

    对页面加载速度影响最大的就是图片,一张普通的图片可以达到几M的大小,而代码也许就只有几十KB.当页面图片很多时,页面的加载速度缓慢,几S钟内页面没有加载完成,也许会失去很多的用户. 所以,对于图片过多 ...

  9. su su- sudo区别概述

    在Linux的操作中经常会用到su 命令进行用户的切换和sudo命令获取root权限,su su- sudo三个命令经常弄混,下面简单的讲解下. 一.查看su的命令帮助信息: pipci@openSU ...

  10. python3之开发环境PyCharm配置

    1. 安装PyCharm(安装时注意选择python),地址: https://www.jetbrains.com/pycharm/ 2. 安装python 地址: https://www.pytho ...