雙向綁定是angular的核心概念之一,它給我們帶來了思維方式的轉變:不再是dom驅動,而是以model為核心,在view中寫上聲明式标簽。然後,angular就會在背景默默的同步view的變化到model,并将model的變化更新到view。
雙向綁定帶來了很大的好處,但是它需要在背景保持一隻“眼睛”,随時觀察所有綁定值的改變,這就是angular 1.x中“性能殺手”的“髒檢查機制”($digest)。可以推論:如果有太多“眼睛”,就會産生性能問題。在讨論優化angular的性能之前,筆者希望先講解下angular的雙向綁定和watchers函數。
為了能夠實作雙向綁定,angular使用了$watch api來監控$scope上的model的改變。angular應用在編譯模闆的時候,會收集模闆上的聲明式标簽 —— 指令或綁定表達式,并連結(link)它們。這個過程中,指令或綁定表達式會注冊自己的監控函數,這就是我們所說的watchers函數。
下面以我們常見的angular表達式(<code>{{}}</code>)為例。
html:
javascript:
這是一個自增長計數器的例子,在上面的代碼我們用了angular表達式(<code>{{}}</code>)。表達式為了能在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表達式(<code>{{}}</code>)背後的源碼如下:
angular會在compile階段收集view模闆上的所有directive。angular表達式會被解析成一種特殊的指令:<code>addtextinterpolatedirective</code>。到了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服務來關閉這些調試資訊。
不僅angular的表達式會使用$scope.$watch api添加watchers,angular内置的大部分指令也一樣,下面再舉幾個常用的angular指令。
ngbind:它和angular表達式很類似,都是綁定特定表達式的值到dom的内容,并保持與scope的同步。不同之處在于它需要一個html節點并以attribute屬性的方式标記。簡單來說,我們可以認為angular表達式就是ngbind的特定文法糖。當然,還是有一點差別的,詳情參見“使用技巧”一章的“防止angular表達式閃爍”。
這裡也能清晰的看見$scope.$watch的注冊代碼:監控器函數為ngbind attribute的值,處理函數則是用表達式計算的結果去更新dom的文本内容。
ngshow/nghide: 它們是根據表達式的計算結果來控制顯示/隐藏dom節點的指令。
這裡同樣用到了$scope.$watch,到這裡你應該明白$watch的工作原理了吧。
再回到上面所提的性能問題。
如果有太多watcher函數,那麼在每次$digest循環時,肯定會慢下來,這就是angular“髒檢查機制”的性能瓶頸。在社群中有個經驗值,如果超過2000個watcher,就可能感覺到明顯的卡頓,特别在ie8這種老舊浏覽器上。有什麼好的方案可以解決這個問題呢?最明顯的方案是:減少$watch,盡量移除不必要的$watch。
要想提高angular頁面的性能,那麼在開發的時候,就應該盡量減少顯式使用$scope.$watch函數,angular中的很多内置指令已經能夠滿足大部分的業務需求。特别是如果能複用ng内置的ui事件指令(ngchange、ngclick…),那麼就不要添加額外的$watch。
對于不再使用的$watch,最好盡早将其釋放,$scope.$watch函數的傳回值就是用于釋放這個watcher的函數,如下面的單次綁定實作(one-time):
在開發中,經常會遇見很多有靜态資料構成的頁面,如靜态的商品、訂單等的顯示,他們在綁定了資料之後,在目前頁面中model不再會被改變。試想我們需要顯示一個教育訓練會議sessions的預約的展示頁面,正常的angular方案應該是用ng-repeat來産生這個清單:
用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後引入的。在官方文檔描述如下:
1.3中為angular表達式(<code>{{}}</code>)引入了新文法,以“::”作為字首的表達式為one-time綁定。對于上面的例子可以改為:
為了讓示例能夠工作,需要引入bindonce庫,并依賴pasvaz.bindonce module。
并把angular表達式改成bo-text指令。該指令将會綁定到model,直到更新dom,然後自動釋放watcher。這樣,顯示功能仍然工作,但不再使用不必要的$watch。在這裡每個session隻有一個$watch綁定,用301個綁定替代了1501個綁定。
恰當的使用bingonce或者1.3的one-time綁定能為應用one程式減少大量不必要$watch綁定,進而提高應用性能。
此刻,我猜你一定正是心中默默嘀咕着:angular“髒檢查機制”一定很慢,一個“肮髒”的家夥。但這是錯誤的。它其實很快,angular團隊為此專門做了很多優化。相反,在大多數場景下,angular這種特殊的watcher機制,反而比很多基于javascript模闆引擎(underscore、handlebars等)更快。因為angular并不需要通過大範圍的dom操作來更新view,它的每次更新區域更小,dom操作更少。而dom操作的代價遠遠高過javascript運算,在有些浏覽器中,修改dom的速度甚至會比純粹的javascript運算慢很多倍!
而且,在現實場景中,我們的大多數頁面都不會超出2000個watcher,因為過多的資訊對使用者是非常不友好的,好的設計師都懂得限制單頁資訊的展示量。但是如果超過了2000個watcher,那麼你就得仔細思考如何去優化它了,應該優先選擇從使用者體驗方面改進,實在不行就用上面提到的技巧來優化你的應用程式。
最後,随着angular 2.0架構對“髒檢查機制”的改進,運作性能将會得到顯著地提高,特别是針對mobile開發的ionic這類架構,将直接受益。