优化Angular应用的性能

优化Angular应用的性能git



MVVM框架的性能,其实就取决于几个因素:程序员


  • 监控的个数github

  • 数据变动检测与绑定的方式数组

  • 索引的性能浏览器

  • 数据的大小数据结构

  • 数据的结构架构


咱们要优化Angular项目的性能,也须要从这几个方面入手。框架


1. 减小监控值的个数性能


监控值的个数怎么减小呢?优化


考虑极端状况,在不引入Angular的时候,监控的个数是为0的,每当咱们有须要绑定的数据项,就产生了监控值。


咱们注意到,Angular里面使用了一种HTML模板语法来作绑定,开发业务项目很是方便,但考虑一下,这种所谓的“模板”,其实与咱们常见的那种模板是不一样的。


传统的模板,是静态模板,将数据代入模板以后生成界面,以后数据再有变化,界面也不会变。但Angular的这种“模板”是动态的,当界面生成完毕,数据产生变动的时候,界面仍是会更新。


这是Angular的优点,但咱们有时候也会由于使用不当,反而增长困扰。由于Angular采用了变更检测的方式来跟踪数据的变化,这些事情都是有负担的,不少时候,有些数据在初始化以后就再也不会变化,但由于咱们没有把它们区分出来,Angular仍是要生成一个监听器来跟踪这部分数据的变化,性能也就受到牵累。


在这种状况下,能够采用单次绑定,仅在初始化的时候把这些数据绑定,语法以下:


<div>{{::item}}</div>


<ul>  

  <li ng-repeat="item in ::items">{{item}}</li>

</ul>


这样的数据就不会被持续观测,也就有效减小了监控值的数目,提升了性能。


2. 下降数据比对的开销


这一个环节是从数据变动检测与绑定的方式入手。细节不说太多了,以前都说过。从数据到界面的更新,通常就两种方式:推、拉。


所谓推,就是在set的时候,主动把与之相关的数据更新,大部分框架是这种方式,低版本浏览器用defineSetter之类。


function Employee() {

    this._firstName = "";

    this._lastName = "";

 

    this.fullName = "";

}

 

Employee.prototype = {

    get firstName(){

        return this._firstName;

    },

    set firstName(val){

        this._firstName = val;

        this.fullName = val + " " + this.lastName;

    },

    get lastName(){

        return this._lastName;

    },

    set lastName(val){

        this._lastName = val;

        this.fullName = this.lastName + " " + val;

    }

};


所谓拉,就是set的时候只改变本身,关联数据等到用的时候本身去取。好比:


function Employee() {

    this.firstName = "";

    this.lastName = "";

}

 

Employee.prototype = {

    get fullName() {

        return this.firstName + " " + this.lastName;

    }

};


有些框架中,两种方式均可以用。这时候能够本身考虑下适合用哪一种方式,好比说,可能有些框架是合并变动,批量更新的,可能就用拉的方式效率高;有些框架是实时变更,差别更新的,那可能就是用推的效率高些。


上面的代码能看出来,从代码编写的简洁性来讲,拉模式要比推模式简单不少,若是能预知数据量较小,能够这样用。


在实际开发过程当中,这两种方式是须要权衡的。咱们举的这个例子比较简单,若是说某个属性依赖于不少东西,例如,一个很大的购物列表,有个总价,它是由每一个商品的单价乘以购买个数,再累加起来的。


在这种状况下,若是使用拉模式,也就是在总价的get上作这个变更,它须要遍历整个数组,从新做计算。可是若是使用推模式,每次有商品价格或者商品购买个数发生变动的时候,都只要在原先的总价上,减去两次变更的差价便可。


此外,不一样的框架用不一样方式来检测数据的变更,好比Angular,若是有一个数组中的元素发生变化了,它是怎样知道这个数组变了呢?


它须要保持变更以前的数据,而后做比对:


  • 首先比对数组的引用是否相等,这一步是为了检测数组的总体赋值,好比this.arr = [1, 2, 3]; 直接把原来的替换掉了,若是出现这种状况,就认为它确定变化了。(其实,若是内容与原先相同,是能够认为没有变的,但由于这些框架的内部实现,每每都须要更新数据与DOM元素的索引关系,因此不能这样)

  • 其次,比较数组的长度,若是长度跟原先不相等了,那确定也产生变化了

  • 而后只能挨个去比对里面元素的变化了


因此,会有人考虑在Angular中结合immutable这样的东西,加速变动的断定过程,由于immutable的数据只要发生任何变化,其引用都必定会变,因此只要第一步断定引用就足以知道数据是否改变了。


有人说,你这个断定下降的开销并不大啊,由于引入immutable要增长复制的开销,跟这里的新旧数据比对开销相比,也低不到哪里去。但这个地方要注意,Angular在有事件产生的时候,会把全部监控数据都从新比对,也就是说,若是你在界面上有个大数组,你从未对它从新赋值,而是常常在另一个很小的表单项绑定的数据上进行更新,这个数组也是要被比对的,这就比较坑了,因此若是引入immutable,能够大幅下降平时这种不受影响时候的比对成本。


可是引入immutable也会对整个应用形成影响,须要在每一个赋值取值的地方都使用immutable的封装方式,并且还要在绑定的时候,对数据做解包,由于Angular绑定的数据是pojo。


因此,用这种方式仍是要慎重,除非框架自身就构建在immutable的基础上。或许,咱们能够指望有一套与ng-model平行的机制,ng-immutable之类,实现的难度也仍是挺大的。


在使用ES5的场景下,能够利用一些方法加速判断,好比数组的:


  • filter

  • map

  • reduce


它们可以返回一个全新的数组,与原先的引用不等,因此在第一步判断就能够得出结果,没必要继续后面几步的比较。


不过,这个环节的优化其实很不明显,最关键的优化在于与之配套的索引优化,参见下一节。


3. 提高索引的性能


在Angular中,能够经过ng-repeat来实现对数组或者对象的遍历,但这个遍历的机制,其实有不少技巧。


在使用简单类型数组的时候,咱们极可能会碰到这么一个问题:数组中存在相同的值,好比:


this.arr = [1, 3, 5, 3];


<ul>

    <li ng-repeat="num in arr">{{num}}</li>

</ul>


这时候会报错,而后若是去搜索一下,会发现一个解决方式:


<ul>

    <li ng-repeat="num in arr track by $index">{{num}}</li>

</ul>


为何这就能解决呢?


咱们先思考一下,若是本身实现相似Angular这样的功能,由于要在DOM和数据之间创建关联,这样,当改变数据的时候,才能刷新到对应的界面,因此,必然有个映射关系。


映射关系须要惟一的索引,在刚才那个例子中,Angular默认对简单类型使用自身当索引,当出现重复的时候,就会出错了。若是指定$index,也就是元素在数组中的下标为索引,就能够避免这个问题。


那么,对于对象数组,又是怎样呢?


好比说这么一个数组,咱们用不一样的两个方式来绑定:


function ListCtrl() {

    this.arr = [];

    for (var i=0; i10000; i++) {

        this.arr.push({

            id: i,

            label: "Item " + i

        });

    }

 

    var time = new Date();

    $timeout(function() {

        alert(new Date() - time);

        console.log(this.arr[0]);

    }.bind(this), 0);

}


<ul ng-controller="ListCtrl as listCtrl">

    <li ng-repeat="item in listCtrl.arr">{{item}}</li>

</ul>


<ul ng-controller="ListCtrl as listCtrl">

    <li ng-repeat="item in listCtrl.arr track by item.id">{{item}}</li>

</ul>


看示例地址,多点击几下:


咱们惊奇地发现,这两个时间有不小差异。


关注一下在绑定以后,arr里面的数据,发如今没有加track by $index的时候,原始数据被改变了,添加了一些索引信息,这些索引是当数据产生变动时,Angular可以找到关联界面的重要线索。


Object {id: 0, label: "Item 0", $$hashKey: "object:4"}


若是咱们知道数据的惟一性由什么保证,而且手动指定其为索引,能够减小没必要要的添加索引的过程。


4. 下降数据的大小


看到这个标题,可能有人会感到奇怪。业务数据的大小并非由程序员控制的,怎么下降呢?这里的下降,指的是下降那些被用于绑定到界面的数据大小。


数据的大小也会影响绑定效率,咱们考虑一个屏幕能展现的数据有限,并不须要把全部东西都当即展现出来,能够从数据中截取一段进行展现,好比你们都熟悉的数据分页就是这么一种方式。


很传统的那种数据分页,是会有一个分页条,上面写着总共多少数据,而后上一页,下一页,这样切换。后来出现了一些变种,好比滚动加载,当滚动条滚到底部的时候,再去加载或生成新的界面。


若是说,咱们有上万条数据造成的一个列表,可是又不打算用那么老圡的方式放个分页条在下面,如何在性能与体验中取得一个平衡呢?


接触过Adobe Flex的人,可能会对其中的列表控件印象深入,由于就算你给它上百万数据,它也不会所以而慢下来,为何呢?由于它的滚动条是假的。


同理,咱们也可能在浏览器中使用DOM来模拟一个滚动条,而后利用这个滚动条的位置,从全量数据中获取对应的那一段数据,而且绑定渲染到界面上。


这种技术通常称为Virtual List,在不少框架中都有第三方实现,能够参见这篇文章:AngularJS virtual list directive tutorial


上面这篇文章作到的,只是初步的优化,并不精细,由于它假定列表中全部项的大小是一致的,并且要在建立阶段即已预知,这样就很不灵活了。若是须要作更精细的优化,须要作实时的度量,对每一个已建立并渲染的子项做度量,而后以此来更新滚动区的位置。


参见demo:http://codepen.io/xufei/pen/avRjqV


5. 将数据的结构扁平化


那么,数据的结构又是怎样影响到执行效率的呢?我举一个常见的例子就是树形结构,这个结构通常人会使用ul和li之类的结构作,而后不可避免地要用递归的方式来使用MVVM框架。


咱们考虑一下,为何非要使用这种方式呢?其缘由有二:


  • 给定的数据结构就是树形的

  • 咱们习惯于使用树形DOM结构来表达树形数据


这个树形数据对咱们来讲,是什么?是数据模型。可是咱们知道,比对两个树形结构是很麻烦的,它的层级使得监控变得复杂,不管是数据的逐一比对,仍是存取器、或者刚被取消的observe提案,都会比单层数据麻烦不少。


若是咱们想要用一种更加扁平的DOM结构来展现它,而不是层级结构,怎么办呢?所谓的树形DOM结构,能展示给咱们的无非是位置的偏移,好比全部下级节点比上级更靠右,这些东西其实能够很轻易使用定位来模拟,这么一来,就有可能适用平级DOM结构来表达树的形状了。


回忆一下,MVVM,这几个字母什么意思?


Model View ViewModel


咱们看了前二者了,但从未关注过视图模型。在不少人眼里,视图模型只是模型的一个简单封装,其实那只是特例,Angular官方的demo造成了这种误导。视图模型的真正做用应当包括:把模型转化为适合视图展现的格式。


若是说咱们须要在视图层有比较扁平的数据结构,就必须在这一层把原始数据拍扁,举个栗子,咱们要作一个动态的组织架构图,这个展开会像一个树,内部确定也会有树形的数据结构,但咱们能够同时维护树形和扁平的两种结构,而且随时保持同步:


原始数据以下:


var source = [

    {id: "0", name: "a"},

    {id: "1", name: "b"},

    {id: "013", name: "abd", parent: "01"},

    {id: "2", name: "c"},

    {id: "3", name: "d"},

    {id: "00", name: "aa", parent: "0"},

    {id: "01", name: "ab", parent: "0"},

    {id: "02", name: "ac", parent: "0"},

    {id: "010", name: "aba", parent: "01"},

    {id: "011", name: "abb", parent: "01"},

    {id: "012", name: "abc", parent: "01"}

];


转换代码以下:


var map = {};

var dest = [];

 

source.forEach(function(it) {

    map[it.id] = it;

});

 

source.forEach(function(it) {

    if (it.parent) {

        //根节点

        dest.push(it);

    }

    else {

        //叶子节点

        map[it.parent].children = map[it.parent].children || [];

        map[it.parent].children.push(it);

    }

});


转换以后的dest变成了这样:


[

    {

        "id": "0",

        "name": "a",

        "children": [

            {

                "id": "00",

                "name": "aa",

                "parent": "0"

            },

            {

                "id": "01",

                "name": "ab",

                "parent": "0",

                "children": [

                    {

                        "id": "013",

                        "name": "abd",

                        "parent": "01"

                    },

                    {

                        "id": "010",

                        "name": "aba",

                        "parent": "01"

                    },

                    {

                        "id": "011",

                        "name": "abb",

                        "parent": "01"

                    },

                    {

                        "id": "012",

                        "name": "abc",

                        "parent": "01"

                    }

                ]

            },

            {

                "id": "02",

                "name": "ac",

                "parent": "0"

            }

        ]

    },

    {

        "id": "1",

        "name": "b"

    },

    {

        "id": "2",

        "name": "c"

    },

    {

        "id": "3",

        "name": "d"

    }

]


咱们在界面绑定的时候仍然使用source,而在操做的时候使用dest。由于,绑定的时候,没必要去通过深层检测,而操做的时候,须要有父子关系来使得操做便利。


好比说,咱们要作一个树状拓扑图,或者是MindMap这类产品,若是不做这样的考虑,极可能会直接把界面结构绑定到树状数据上,这时候效率相对会比较低些。


但咱们也能够做这种优化:


  • 同时保存扁平化的原始数据,也生成树状数据

  • 把展现结构绑定到扁平化的数据上

  • 每当结构变动的时候,在树状数据上更新,而且在数据模型内部计算出界面坐标

  • 展现结构的扁平数据由于跟树状数据是相同引用,也被更新了,也就引起界面刷新

  • 这时候,界面是单层刷新,无需跟踪层级数据,效率能够提升很多,尤为在层次较深的时候


6. 小结


MVVM存在的意义就是尽量提升开发效率,只有很极端状况下值得去优化性能。若是你的场景中出现很是多的性能问题,极可能是不适合用这类框架的业务形态。


总结一下咱们的几种优化方式,他们的机制分别是:


  • 减小监控项

  • 加快变动检测速度

  • 主动设置索引

  • 缩小渲染的数据量

  • 数据的扁平化


能够看到,咱们全部的优化都是在数据层面,没必要刻意去优化界面。若是你用了一个MVVM框架,却为它做了各类各样至关多的优化,那还不如不要用它,全手工写。


针对其余MVVM框架,也大体能够用相似的几种方式,只是部分细节有差别,能够举一反三。

本文转自做者:徐飞(@民工精髓V) 网址:https://github.com/xufei/blog/issues/23
若有侵权请联系公众号:数通畅联,将会第一时间删除。

相关文章
相关标签/搜索