雙向綁定是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 |
|
JavaScript:
1 2 3 4 5 6 7 8 9 |
|
這是一個自增長計數器的例子,在上面的代碼我們用了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 |
|
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 |
|
其它指令中的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 |
|
這里也能清晰的看見$scope.$watch的注冊代碼:監控器函數為ngBind attribute的值,處理函數則是用表達式計算的結果去更新DOM的文本內容。
ngShow/ngHide: 它們是根據表達式的計算結果來控制顯示/隱藏DOM節點的指令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
這里同樣用到了$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 |
|
one-time綁定
在開發中,經常會遇見很多有靜態數據構成的頁面,如靜態的商品、訂單等的顯示,他們在綁定了數據之后,在當前頁面中Model不再會被改變。試想我們需要顯示一個培訓會議Sessions的預約的展示頁面,常規的Angular方案應該是用ng-repeat來產生這個列表:
HTML:
1 2 3 4 5 6 7 8 9 10 11 |
|
JavaScript:
1 2 3 4 5 6 7 |
|
用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 |
|
在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 |
|
為了讓示例能夠工作,需要引入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 |
|
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 |
|
可以在這里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這類框架,將直接受益。