優化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
若有侵權請聯繫公衆號:數通暢聯,將會第一時間刪除。

相關文章
相關標籤/搜索