Angular移除不必要的$watch之性能優化

来源:http://www.cnblogs.com/wulihe/archive/2017/11/23/7884382.html
-Advertisement-
Play Games

雙向綁定是Angular的核心概念之一,它給我們帶來了思維方式的轉變:不再是DOM驅動,而是以Model為核心,在View中寫上聲明式標簽。然後,Angular就會在後臺默默的同步View的變化到Model,並將Model的變化更新到View。 雙向綁定帶來了很大的好處,但是它需要在後臺保持一隻“眼 ...


Angular-apply-and-瀏覽器-event-loop

雙向綁定是Angular的核心概念之一,它給我們帶來了思維方式的轉變:不再是DOM驅動,而是以Model為核心,在View中寫上聲明式標簽。然後,Angular就會在後臺默默的同步View的變化到Model,並將Model的變化更新到View。

雙向綁定帶來了很大的好處,但是它需要在後臺保持一隻“眼睛”,隨時觀察所有綁定值的改變,這就是Angular 1.x中“性能殺手”的“臟檢查機制”($digest)。可以推論:如果有太多“眼睛”,就會產生性能問題。在討論優化Angular的性能之前,筆者希望先講解下Angular的雙向綁定和watchers函數。

雙向綁定和watchers函數

為了能夠實現雙向綁定,Angular使用了$watch API來監控$scope上的Model的改變。Angular應用在編譯模板的時候,會收集模板上的聲明式標簽 —— 指令或綁定表達式,並鏈接(link)它們。這個過程中,指令或綁定表達式會註冊自己的監控函數,這就是我們所說的watchers函數。

下麵以我們常見的Angular表達式({{}})為例。

HTML:

1
2
3
4
<body ng-app="com.ngnice.app" ng-controller="DemoController as demo">
    <div>hello : {{demo.count}}</div>
    <button type="button" ng-click="demo.increase ();">increase ++</button>
</body>

JavaScript:

1
2
3
4
5
6
7
8
9
angular.module('com.ngnice.app')
.controller('DemoController', [function() {
  var vm = this;
  vm.count = 0;
  vm.increase = function() {
    vm.count++;
  };
  return vm;
}]);

這是一個自增長計數器的例子,在上面的代碼我們用了Angular表達式({{}})。表達式為了能在Model的值改變的時候你能及時更新View,它會在其所在的$scope(本例中為DemoController)中註冊上面提到的watchers函數,監控count屬性的變化,以便及時更新View。

上例中在每次點擊button的時候,count計數器將會加1,然後count的變化會通過Angular的$digest過程同步到View之上。在這裡它是一個單向的更新,從Model到View的更新。如果處理一個帶有ngModel指令的input交互控制項,則在View上的每次輸入都會被及時更新到Model之上,這裡則是反向的更新,從View到Model的更新。

Model數據能被更新到View是因為在背後默默工作的$digest迴圈(“臟檢查機制”)被觸發了。它會執行當前scope以及其所有子scope上註冊的watchers函數,檢測是否發生變化,如果變了就執行相應的處理函數,直到Model穩定了。如果這個過程中發生過變化,瀏覽器就會重新渲染受到影響的DOM來體現Model的變化。

在Angular表達式({{}})背後的源碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function collectDirectives(node, directives, attrs, maxPriority, ignoreDirective) {
  var nodeType = node.nodeType,
    attrsMap = attrs.$attr,
    match,
    className;
  switch (nodeType) {
    case 1:
      /* Element */
      ...
      break;
    case 3:
      /* Text Node */
      addTextInterpolateDirective(directives, node.nodeValue);
      break;
    case 8:
      /* Comment */
      ...
      break;
  }
  directives.sort(byPriority);
  return directives;
}
function addTextInterpolateDirective(directives, text) {
  var interpolateFn = $interpolate(text, true);
  if (interpolateFn) {
    directives.push({
      priority: 0,
      compile: function textInterpolateCompileFn(templateNode) {
        // when transcluding a template that has bindings in the root
        // then we don't have a parent and should do this in the linkFn
        var parent = templateNode.parent(),
          hasCompileParent = parent.length;
        if (hasCompileParent) safeAddClass(templateNode.parent(), 'ng-binding');
        return function textInterpolateLinkFn(scope, node) {
          var parent = node.parent(),
            bindings = parent.data('$binding') || [];
          bindings.push(interpolateFn);
          parent.data('$binding', bindings);
          if (!hasCompileParent) safeAddClass(parent, 'ng-binding');
          scope.$watch(interpolateFn, function interpolateFnWatchAction(value) {
            node[0].nodeValue = value;
          });
        };
      }
    });
  }
}

Angular會在compile階段收集View模板上的所有Directive。Angular表達式會被解析成一種特殊的指令:addTextInterpolateDirective。到了link階段,就會利用scope.$watch的API註冊我們在上面提到的watchers函數:它的求值函數為$interpolate對綁定表達式進行編譯的結果,監聽函數則是用新的表達式計算值去修改DOM Node的nodeValue。可見,在View中的Angular表達式,也會成為Angular在$digest迴圈中watchers的一員。

在上面代碼中,還有一部分是為了給調試器用的。它會在Angular表達式所屬的DOM節點加上名為‘ng-binding’的調試類。類似的調試類還有‘ng-scope’,‘ng-isolate-scope’等。在Angular 1.3中我們可以使用compileProvider服務來關閉這些調試信息。

1
2
3
4
app.config(['$compileProvider', function ($compileProvider) {
  // disable debug info
  $compileProvider.debugInfoEnabled(false);
}]);

其它指令中的watchers函數

不僅Angular的表達式會使用$scope.$watch API添加watchers,Angular內置的大部分指令也一樣,下麵再舉幾個常用的Angular指令。

ngBind:它和Angular表達式很類似,都是綁定特定表達式的值到DOM的內容,並保持與scope的同步。不同之處在於它需要一個HTML節點並以attribute屬性的方式標記。簡單來說,我們可以認為Angular表達式就是ngBind的特定語法糖。當然,還是有一點區別的,詳情參見“使用技巧”一章的“防止Angular表達式閃爍”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var ngBindDirective = ngDirective({
  compile: function(templateElement) {
    templateElement.addClass('ng-binding');
    return function (scope, element, attr) {
      element.data('$binding', attr.ngBind);
      scope.$watch(attr.ngBind, function ngBindWatchAction(value) {
        // We are purposefully using == here rather than === because we want to
        // catch when value is "null or undefined"
        // jshint -W041
        element.text(value == undefined ? '' : value);
      });
    };
  }
});

這裡也能清晰的看見$scope.$watch的註冊代碼:監控器函數為ngBind attribute的值,處理函數則是用表達式計算的結果去更新DOM的文本內容。

ngShow/ngHide: 它們是根據表達式的計算結果來控制顯示/隱藏DOM節點的指令。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ngShowDirective = ['$animate', function($animate) {
  return function(scope, element, attr) {
    scope.$watch(attr.ngShow, function ngShowWatchAction(value){
      $animate[toBoolean(value) ? 'removeClass' : 'addClass'](element, 'ng-hide');
    });
  };
}];
var ngHideDirective = ['$animate', function($animate) {
  return function(scope, element, attr) {
    scope.$watch(attr.ngHide, function ngHideWatchAction(value){
      $animate[toBoolean(value) ? 'addClass' : 'removeClass'](element, 'ng-hide');
    });
  };
}];

這裡同樣用到了$scope.$watch,到這裡你應該明白$watch的工作原理了吧。

再回到上面所提的性能問題。

如果有太多watcher函數,那麼在每次$digest迴圈時,肯定會慢下來,這就是Angular“臟檢查機制”的性能瓶頸。在社區中有個經驗值,如果超過2000個watcher,就可能感覺到明顯的卡頓,特別在IE8這種老舊瀏覽器上。有什麼好的方案可以解決這個問題呢?最明顯的方案是:減少$watch,儘量移除不必要的$watch。

慎用$watch和及時銷毀

要想提高Angular頁面的性能,那麼在開發的時候,就應該儘量減少顯式使用$scope.$watch函數,Angular中的很多內置指令已經能夠滿足大部分的業務需求。特別是如果能復用ng內置的UI事件指令(ngChange、ngClick…),那麼就不要添加額外的$watch。

對於不再使用的$watch,最好儘早將其釋放,$scope.$watch函數的返回值就是用於釋放這個watcher的函數,如下麵的單次綁定實現(one-time):

1
2
3
4
5
6
7
8
9
10
11
12
angular.module('com.ngnice.app')
.controller('DemoController', ['$scope', function($scope) {
  var vm = this;
  vm.count = 0;
  var textWatch = $scope.$watch('demo.updated', function(newVal, oldVal) {
    if (newVal !== oldVal) {
      vm.count++;
      textWatch();
    }
  });
  return vm;
}]);

one-time綁定

在開發中,經常會遇見很多有靜態數據構成的頁面,如靜態的商品、訂單等的顯示,他們在綁定了數據之後,在當前頁面中Model不再會被改變。試想我們需要顯示一個培訓會議Sessions的預約的展示頁面,常規的Angular方案應該是用ng-repeat來產生這個列表:

HTML:

1
2
3
4
5
6
7
8
9
10
11
<ul>
    <li ng-repeat="session in sessions">
        <div class="info">
            {{session.name}} - {{session.room}} - {{session.hour}} - {{session.speaker}}
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

JavaScript:

1
2
3
4
5
6
7
angular.module('com.ngnice.app')
.controller('MainController', ['$scope', function($scope) {
  $scope.sessions = [...];
  $scope.likeSession = function(session) {
    // Like the session
  }
}]);

用Angular來實現這個需求,很簡單。但假設這是一個大型的預約,一天會有300個Sessions。那麼這裡會產生多少個$watch?這裡每個Session有5個綁定,額外的ng-repeat一個。這將會產生1501個$watch。這有什麼問題?每次用戶“like”一個Session,Angular將會去檢查name、room等5個屬性是不是被改變了。

問題在於,除了例外的“like”外,所有數據都是靜態數據,這是不是有點浪費資源?我們知道數據Model是沒有被改變的,既然這樣為什麼讓Angular要去檢查是否改變呢?

因此,這裡的$watch是沒必要的,它的存在反而會影響$digest的性能,但這個$watch在第一次卻是必要的,它在初始化時用靜態信息填充了我們的DOM結構。對於這類情況,如果能換為單次(one-time)綁定應該是最佳的方案。

Angular中的單次(one-time)綁定是在1.3後引入的。在官方文檔描述如下:

單次表達式在第一次$digest完成後,將不再計算(監測屬性的變化)。

1.3中為Angular表達式({{}})引入了新語法,以“::”作為首碼的表達式為one-time綁定。對於上面的例子可以改為:

1
2
3
4
5
6
7
8
9
10
11
<ul>
    <li ng-repeat="session in sessions">
        <div class="info">
            {{::session.name}} - {{::session.room}} - {{::session.hour}} - {{::session.speaker}}
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

在1.3之前的版本沒有提供這個語法,我們應該如何實現one-time綁定呢?在開源社區中有個牛人在我們之前也問了自己這個問題,並創建了一系列指令來實現它:Bindonce https://github.com/Pasvaz/bindonce。用Bindonce實現如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<ul>
    <li bindonce ng-repeat="session in sessions">
        <div class="info">
            <span bo-text="session.name"></span> -
            <span bo-text="session.room"></span> -
            <span bo-text="session.hour"></span> -
            <span bo-text="session.speaker"></span>
        </div>
        <div class="likes">
            {{session.likes}} likes!
            <button ng-click="likeSession(session)">Like it!</button>
        </div>
    </li>
</ul>

為了讓示例能夠工作,需要引入bindonce庫,並依賴pasvaz.bindonce module。

angular.module('com.ngnice.app', ['pasvaz.bindonce']);

並把Angular表達式改成bo-text指令。該指令將會綁定到Model,直到更新DOM,然後自動釋放watcher。這樣,顯示功能仍然工作,但不再使用不必要的$watch。在這裡每個Session只有一個$watch綁定,用301個綁定替代了1501個綁定。

恰當的使用bingonce或者1.3的one-time綁定能為應用one程式減少大量不必要$watch綁定,從而提高應用性能。

滾屏載入

另外一種可行的性能解決方案就是滾屏載入,又稱”Endless Scrolling,“ “unpagination”,這是用於大量數據集顯示的時候,又不想表格分頁,所以一般放在頁面最底部,當滾動屏幕到達頁面底部的時候,就會嘗試載入一個序列的數據集,追加在頁面底部。在Angular社區有開源組件ngInfiniteScroll http://binarymuse.github.io/ngInfiniteScroll/index.html實現滾屏載入。下麵是官方Demo:

HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div ng-app='myApp' ng-controller='DemoController'>
  <div infinite-scroll='reddit.nextPage()' infinite-scroll-disabled='reddit.busy' infinite-scroll-distance='1'>
    <div ng-repeat='item in reddit.items'>
      <span class='score'>{{item.score}}</span>
      <span class='title'>
        <a ng-href='{{item.url}}' target='_blank'>{{item.title}}</a>
      </span>
      <small>by {{item.author}} -
        <a ng-href='http://reddit.com{{item.permalink}}' target='_blank'>{{item.num_comments}} comments</a>
      </small>
      <div style='clear: both;'></div>
    </div>
    <div ng-show='reddit.busy'>Loading data...</div>
  </div>
</div>

JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var myApp = angular.module('myApp', ['infinite-scroll']);
myApp.controller('DemoController', ['$scope', 'Reddit', function($scope, Reddit) {
  $scope.reddit = new Reddit();
}]);
// Reddit constructor function to encapsulate HTTP and pagination logic
myApp.factory('Reddit', ['$http', function($http) {
  var Reddit = function() {
    this.items = [];
    this.busy = false;
    this.after = '';
  };
  Reddit.prototype.nextPage = function() {
    if (this.busy) return;
    this.busy = true;
    var url = 'http://api.reddit.com/hot?after=' + this.after + '&jsonp=JSON_CALLBACK';
    $http.jsonp(url).success(function(data) {
      var items = data.data.children;
      for (var i = 0; i < items.length; i++) {
        this.items.push(items[i].data);
      }
      this.after = 't3_' + this.items[this.items.length - 1].id;
      this.busy = false;
    }.bind(this));
  };
  return Reddit;
}]);

可以在這裡http://binarymuse.github.io/ngInfiniteScroll/demo_async.html訪問這個例子。其使用很簡單,有興趣的讀者可以查看其官方文檔。

其它

當然對於性能解決方案還有很多,如客戶端分頁、服務端分頁、將其它更高效的jQuery插件或者React插件合理的封裝為ng組件等。當封裝第三方非Angular組件時需要註意scope和model的同步,以及合理的觸發$apply更新View。另外在開源社區中也有ngReact可以簡化將React組件應用到Angular應用中,在這裡可以瞭解到關於它的更多信息:https://github.com/davidchang/ngReact

此刻,我猜你一定正是心中默默嘀咕著:Angular“臟檢查機制”一定很慢,一個“骯髒”的家伙。但這是錯誤的。它其實很快,Angular團隊為此專門做了很多優化。相反,在大多數場景下,Angular這種特殊的watcher機制,反而比很多基於JavaScript模板引擎(underscore、Handlebars等)更快。因為Angular並不需要通過大範圍的DOM操作來更新View,它的每次更新區域更小,DOM操作更少。而DOM操作的代價遠遠高過JavaScript運算,在有些瀏覽器中,修改DOM的速度甚至會比純粹的JavaScript運算慢很多倍!

而且,在現實場景中,我們的大多數頁面都不會超出2000個watcher,因為過多的信息對使用者是非常不友好的,好的設計師都懂得限制單頁信息的展示量。但是如果超過了2000個watcher,那麼你就得仔細思考如何去優化它了,應該優先選擇從用戶體驗方面改進,實在不行就用上面提到的技巧來優化你的應用程式。

最後,隨著Angular 2.0框架對“臟檢查機制”的改進,運行性能將會得到顯著地提高,特別是針對Mobile開發的ionic這類框架,將直接受益。


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

-Advertisement-
Play Games
更多相關文章
  • 最近開發我的介面測試平臺 ,但是遇到了一個需求,需要開發定時任務,於是百度搜索,找到了這麼一個叫 pFlask-APScheduler然後開始了我的第一次的學習,於是乎, 需求是這麼的: 1.添加定時任務, 2.暫停定時任務, 3.恢復定時任務 4.移除定時任務 5.獲取定時任務,。 然後我的代碼可 ...
  • 學完前面的教程,相信你已經能爬取大部分的網站信息了,但是當你爬的網站多了,你應該會發現一個新問題,有的網站需要登錄賬戶才能看到更多的信息對吧?那麼這種網站怎麼爬取呢?這些登錄數據就是今天要說的——cookie cookie 其實在前面在解析requests模塊時也提到過的。 Cookie,指某些網站 ...
  • 1、下載64位rxtx for java 鏈接:http://fizzed.com/oss/rxtx-for-java 2、下載下來的包解壓後按照說明放到JAVA_HOME即JAVA的安裝路徑下麵去 3、在maven的pom.xml下添加 4、串口API CommPort:埠的抽象類 CommPo ...
  • 眾所周知,在我們開發過程當中應用配置文件對於我們來說覺得是一個方便的選擇。當我們遇到不需要經常修改且數據量較少的時候,選擇配置文件可以方便的應用於程式,而不必修改源碼。 那麼我就來簡單介紹一下 python 常用配置文件的格式都有哪些。 ini 格式 我們先創建一個叫 db_config.ini 的 ...
  • 要學習List<E>介面,首先,我知道它還有一個父介面Collection<E>。而Collection<E>又有一個超級介面Iterable<T>。 我們從超級介面Iterable<T>開始看: 方法只有一個: iterator() // 返回一個在一組 T 類型的元素上進行迭代的迭代器。 然後我 ...
  • EOJ2784 Remainder 分析: 參考代碼: #include<iostream>using namespace std;int N,a; int main(){ cin>>N; while(N--){ cin>>a; if(a%2==0)cout<<a*(a-2)<<endl; else ...
  • Pipeline在Jenkins里的作用 最近一直在使用jenkins進行自動化部署的工作,開始覺得很爽,省去了很多重覆的工作,它幫助我自動拉伺服器的代碼,自動還原包包,自動編譯項目,自動發佈項目,自動打包鏡像,自動上傳倉庫,自動啟動docker服務,這一系列動作都是自動化的,聽起來確實很絢,但是, ...
  • Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable介面。 兩種方式都要通過重寫run()方法來定義線程的行為,推薦使用後者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable介面更為靈活 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...