優化Angular應用的性能

来源:http://www.cnblogs.com/agileai/archive/2016/02/17/5194439.html
-Advertisement-
Play Games

MVVM框架的性能,其實就取決於幾個因素: 監控的個數 數據變更檢測與綁定的方式 索引的性能 數據的大小 數據的結構 我們要優化Angular項目的性能,也需要從這幾個方面入手。 1. 減少監控值的個數 監控值的個數怎麼減少呢? 考慮極端情況,在不引入Angular的時候,監控的個數是為0的,每當我


 

MVVM框架的性能,其實就取決於幾個因素:

  • 監控的個數

  • 數據變更檢測與綁定的方式

  • 索引的性能

  • 數據的大小

  • 數據的結構

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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • IO是輸入和輸出的簡稱,在實際的使用時,輸入和輸出是有方向的。就像現實中兩個人之間借錢一樣,例如A借錢給B,相對於A來說是借出,而相對於B來說則是借入。所以在程式中提到輸入和輸出時,也需要區分清楚是相對的內容。 在 程式中,輸入和輸出都是相對於當前程式而言的,例如從硬碟上讀取一個配置文件的內容到程式
  • 一、SSH框架的概念 SSH框架是java web開發流行的一個開源集合框架,SSH框架包含了Struts框架,Spring框架和Hibernate框架。SSH框架可以用於構建靈活、易於擴展的多層Web應用程式。 二、SSH框架的工作原理 SSH框架中的Struts負責攔截用戶請求,正常情況下用戶請
  • 1、路由是程式的方法和URL的一一映射。 在配置文件里,把經常訪問的路由放在前面,可以提高路由匹配的效率。 2、路由匹配的兩種方式 Annotation 允許在方法的上面用註釋定義方法運行狀態的功能 class UserController extends Controller{ /** * @Ro
  • lazy概念:要用到的時候,再去載入,對於關聯的集合來說,只有當訪問到的時候,才去載入它所關聯的集合,比如一個user對應很多許可權,只有當user.getRights()的時候,才發出select right的語句,在訪問到rights之前,rights是一個PersisitSet對於實體類來說,只
  • 1 <?php 2 // 正確地顯示覆數 3 if(!function_exists('_plurals_format')) 4 { 5 /** 6 * 正確的使用複數 7 * @access public 8 * @author zhaoyingnan 2016-02-17 11:53 9 * @
  • pyextend - python extend lib accepts(exception=TypeError, **types) 參數: exception: 檢查失敗時的拋出異常類型 **types: 待檢查的k-v參數 **types參數支持 a=int : 待測函數參數a必須為 int 類
  • 抽象類 1 package com.shejimoshi.structural.Adapter; 2 3 4 /** 5 * 功能:適配器模式 6 * 將一個類的介面轉換成客戶希望的另外一個介面。adapter模式使得原本由於介面不相容而不能一起工作的那些類可以一起工作 7 * 適用性:你想使用一個
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...