摘要:通常寫代碼時我們無需主動調用或是因為在外部對我們的回調函數做了包裝。類似的不只是這些事件回調函數,還有等。常量依舊會重復檢查。會檢查中有沒有一個名為的成員。
TL;DR
臟檢查是一種模型到視圖的數據映射機制,由 $apply 或 $digest 觸發。
臟檢查的范圍是整個頁面,不受區域或組件劃分影響
使用盡量簡單的綁定表達式提升臟檢查執行速度
盡量減少頁面上綁定表達式的個數(單次綁定和ng-if)
給 ng-repeat 添加 track by 讓 angular 復用已有元素
什么是臟數據檢查(Dirty checking)Angular 是一個 MVVM 前端框架,提供了雙向數據綁定。所謂雙向數據綁定(Two-way data binding)就是頁面元素變化會觸發 View-model 中對應數據改變,反過來 View-model 中數據變化也會引發所綁定的 UI 元素數據更新。操作數據就等同于操作 UI。
看似簡單,其實水很深。UI 元素變化引發 Model 中數據變化這個通過綁定對應 DOM 事件(例如 input 或 change)可以簡單的實現;然而反過來就不是那么容易。
比如有如下代碼:
用戶點擊了 button,angular 執行了一個叫 onClick 的方法。這個 onClick 的方法體對于 angular 來說是黑盒,它到底做了什么不知道。可能改了 $scope.content1 的值,可能改了 $scope.content2 的值,也可能兩個值都改了,也可能都沒改。
那么 angular 到底應該怎樣得知 onClick() 這段代碼后是否應該刷新 UI,應該更新哪個 DOM 元素?
angular 必須去挨個檢查這些元素對應綁定表達式的值是否有被改變。這就是臟數據檢查的由來(臟數據檢查以下簡稱臟檢查)。
臟檢查如何被觸發angular 會在可能觸發 UI 變更的時候進行臟檢查:這句話并不準確。實際上,臟檢查是 $digest](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest) 執行的,另一個更常用的用于觸發臟檢查的函數 [$apply 其實就是 $digest 的一個簡單封裝(還做了一些抓異常的工作)。
通常寫代碼時我們無需主動調用 $apply 或 $digest 是因為 angular 在外部對我們的回調函數做了包裝。例如常用的 ng-click,這是一個指令(Directive),內部實現則 類似 于
DOM.addEventListener("click", function ($scope) { $scope.$apply(() => userCode()); });
可以看到:ng-click 幫我們做了 $apply 這個操作。類似的不只是這些事件回調函數,還有 $http、$timeout 等。我聽很多人抱怨說 angular 這個庫太大了什么都管,其實你可以不用它自帶的這些服務(Service),只要你記得手工調用 $scope.$apply。
臟檢查的范圍前面說到:angular 會對所有綁定到 UI 上的表達式做臟檢查。其實,在 angular 實現內部,所有綁定表達式都被轉換為 $scope.$watch()。每個 $watch 記錄了上一次表達式的值。有 ng-bind="a" 即有 $scope.$watch("a", callback),而 $scope.$watch 可不會管被 watch 的表達式是否跟觸發臟檢查的事件有關。
例如:
問:點擊 TEST 這個按鈕時會觸發臟檢查嗎?觸發幾次?
首先:ng-click="" 什么都沒有做。angular 會因為這個事件回調函數什么都沒做就不進行臟檢查嗎?不會。
然后:#span1 被隱藏掉了,會檢查綁定在它上面的表達式嗎?盡管用戶看不到,但是 $scope.$watch("content", callback) 還在。就算你直接把這個 span 元素干掉,只要 watch 表達式還在,要檢查的還會檢查。
再次:重復的表達式會重復檢查嗎?會。
最后:別忘了 ng-show="false"。可能是因為 angular 的開發人員認為這種綁定常量的情況并不多見,所以 $watch 并沒有識別所監視的表達式是否是常量。常量依舊會重復檢查。
所以:
答:觸發三次。一次 false,一次 content,一次 content
所以說一個綁定表達式只要放在當前 DOM 樹里就會被監視,不管它是否可見,不管它是否被放在另一個 Tab 里,更不管它是否與用戶操作相關。
另外,就算在不同 Controller 里構造的 $scope 也會互相影響,別忘了 angular 還有全局的 $rootScope,你還可以 $scope.$emit。angular 無法保證你絕對不會在一個 controller 里更改另一個 controller 生成的 scope,包括 自定義指令(Directive)生成的 scope 和 Angular 1.5 里新引入的組件(Component)。
所以說不要懷疑用戶在輸入表單時 angular 會不會監聽頁面左邊導航欄的變化。
臟檢查與運行效率臟檢查慢嗎?
說實話臟檢查效率是不高,但是也談不上有多慢。簡單的數字或字符串比較能有多慢呢?十幾個表達式的臟檢查可以直接忽略不計;上百個也可以接受;成百上千個就有很大問題了。綁定大量表達式時請注意所綁定的表達式效率。建議注意一下幾點:
表達式(以及表達式所調用的函數)中少寫太過復雜的邏輯
不要連接太長的 filter(往往 filter 里都會遍歷并且生成新數組)
不要訪問 DOM 元素。
使用單次綁定減少綁定表達式數量單次綁定(One-time binding 是 Angular 1.3 就引入的一種特殊的表達式,它以 :: 開頭,當臟檢查發現這種表達式的值不為 undefined 時就認為此表達式已經穩定,并取消對此表達式的監視。這是一種行之有效的減少綁定表達式數量的方法,與 ng-repeat 連用效果更佳(下文會提到),但過度使用也容易引發 bug。
善用 ng-if 減少綁定表達式的數量如果你認為 ng-if 就是另一種用于隱藏、顯示 DOM 元素的方法你就大錯特錯了。
ng-if 不僅可以減少 DOM 樹中元素的數量(而非像 ng-hide 那樣僅僅只是加個 display: none),每一個 ng-if 擁有自己的 scope,ng-if 下面的 $watch 表達式都是注冊在 ng-if 自己 scope 中。當 ng-if 變為 false,ng-if 下的 scope 被銷毀,注冊在這個 scope 里的綁定表達式也就隨之銷毀了。
考慮這種 Tab 選項卡實現:
對于這種會反復隱藏、顯示的元素,通常人們第一反應都是使用 ng-show 或 ng-hide 簡單的用 display: none 把元素設置為不可見。
然而入上文所說,肉眼不可見不代表不會跑臟檢查。如果將 ng-show 替換為 ng-if 或 ng-switch-when
[[Tab 1 body...]][[Tab 2 body...]][[Tab 3 body...]][[Tab 4 body...]]
有如下優點:
首先 DOM 樹中的元素個數顯著減少至四分之一,降低內存占用
其次 $watch 表達式也減少至四分之一,提升臟檢查循環的速度
如果這個 tab 下面有 controller(例如每個 tab 都被封裝為一個組件),那么僅當這個 tab 被選中時該 controller 才會執行,可以減少各頁面的互相干擾
如果 controller 中調用接口獲取數據,那么僅當對應 tab 被選中時才會加載,避免網絡擁擠
當然也有缺點:
DOM 重建本身費時間
如果 tab 下有 controller,那么每次該 tab 被選中時 controller 都會被執行
如果在 controller 里面調接口獲取數據,那么每次該 tab 被選中時都會重新加載
各位讀者自己取舍。
當臟檢查遇上數組ng-repeat!這就更有(e)趣(xin)了。通常的綁定只是去監聽一個值的變化(綁定對象也是綁定到對象里的某個成員),而 ng-repeat 卻要監視一整個數組對象的變化。例如有:
會生成 4 個 li 元素
1
2
3
4
沒有問題。如果我添加一個按鈕如下:
請考慮:當用戶點擊這個按鈕會發生什么?
我們一步一步分析。開始的時候,angular 記錄了 array 的初始狀態為:
[ { "value": 1 }, { "value": 2 }, { "value": 3 }, { "value": 4 } ]
當用戶點擊按鈕后,數組的第一個元素被刪除了,array 變為:
[ { "value": 2 }, { "value": 3 }, { "value": 4 } ]
兩者比較:
array.length = 4 => array.length = 3
array[0].value = 1 => array[0].value = 2
array[1].value = 2 => array[1].value = 3
array[2].value = 3 => array[2].value = 4
array[3].value = 4 => array[3].value = undefined (array[4] 為 undefined,則 undefined.value 為 undefined,見 Angular 表達式的說明)
如同你所見:angular 經過比較,看到的是:
數組長度減少了 1
數組第 1 個元素的 value 被改為 2
數組第 2 個元素的 value 被改為 3
數組第 3 個元素的 value 被改為 4
反應到 DOM 元素上就是:
第 1 個 li 內容改為 2
第 2 個 li 內容改為 3
第 3 個 li 內容改為 4
第 4 個 li 刪掉
可以看到,刪除一個元素導致了整個 ul 序列的刷新。要知道 DOM 操作要比 JS 變量操作要慢得多,類似這樣的無用操作最好能想辦法避免。
那么問題出在哪里呢?用戶刪除了數組的第一個元素,導致了整個數組元素前移;然而 angular 沒法得知用戶做了這樣一個刪除操作,只能傻傻的按下標一個一個比。
那么只要引入一種機制來標記數組的每一項就好了吧。于是 angular 引入了 track by
詳解 track by用來標記數組元素的一定是數組里類似 ID 的某個值。這個值一定要符合以下這兩個特點。
不能重復。ID 重復了什么鬼
值一定要簡單。ID 是用于比較相等的,有時候由于算法不同可能還要比較大小,處于速度考慮不能太復雜。
基于這兩個特點。如果用戶沒有給 ng-repeat 指定 track by 的表達式,則默認為內置函數 $id。$id 會檢查 item 中有沒有一個名為 $$hashKey` 的成員。如有,返回其值;如沒有,則生成一個新的唯一值寫入。這就是數組中那個奇怪的 `$$hashKey 成員來歷,默認值是 "object:X"(你問我為什么是個字符串而不是數字?我怎么知道。。。)
還是前面的問題,引入 track by 后再來看。因為沒有指定 track by,則默認為 $id(item),實際為 $$hashKey。
開始的時候,$id(item) 給數組中所有項創建了 $$hashKey
這時 angular 記錄了 array 的初始狀態為:
[ { "value": 1, "$$hashKey": "object:1" }, { "value": 2, "$$hashKey": "object:2" }, { "value": 3, "$$hashKey": "object:3" }, { "value": 4, "$$hashKey": "object:4" } ]
當用戶點擊按鈕后,數組的第一個元素被刪除了,array 變為:
[ { "value": 2, "$$hashKey": "object:2" }, { "value": 3, "$$hashKey": "object:3" }, { "value": 4, "$$hashKey": "object:4" } ]
先比較 track by 的元素,這里為 $id(item),即 $$hashKey
"object:1" => "object:2"
"object:2" => "object:3"
"object:3" => "object:4"
"object:4" => undefined
兩者對不上,說明數組被做了增刪元素或者移動元素的操作。將其規整
"object:1" => undefined
"object:2" => "object:2"
"object:3" => "object:3"
"object:4" => "object:4"
那么顯然,第一個元素被刪除了。再比較剩余的元素
array[0].value = 2 => array[0].value = 2
array[1].value = 3 => array[1].value = 3
array[2].value = 4 => array[2].value = 4
結論是:
原數組第一個元素被刪除
其他沒變
angular 通過將新舊數組的 track by 元素做 diff 猜測用戶的行為,最大可能的減少 DOM 樹的操作,這就是 track by 的用處。
默認 track by 的坑So far so good! 然而需求某天有變,程序員小哥決定用 filter 給數組做 map 后再渲染。
map 定義如下:
xxModule.filter("map", function () { return arr => arr.map(item => ({ value: item.value + 1 })); });
ng-repeat 執行時先計算表達式 array | myMap 的值:
arrayForNgRepeat = [ { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }, ]
注意數組 arrayForNgRepeat 和原來的數組 array 不是同一個引用,因為 filter 里的 map 操作生成了一個新數組,每一項都是新對象,跟原數組無關。
ng-repeat 時,angular 發現用戶沒有指定 track by,按照默認邏輯,使用 $id(item) 作為 track by,添加 $$hashKey
arrayForNgRepeat = [ { value: 2, "$$hashKey": "object:1" }, { value: 3, "$$hashKey": "object:2" }, { value: 4, "$$hashKey": "object:3" }, { value: 5, "$$hashKey": "object:4" }, ]
生成 DOM:
2
3
4
5
這里請再次注意:數組 arrayForNgRepeat 與原始數組 array 沒有任何關系,數組本身是不同的引用,數組里的每一項也是不同引用。修改新數組的成員不會影響到原來的數組。
這時 array 的值:
array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, ]
這時用戶的某個無關操作觸發了臟檢查。針對 ng-repeat 表達式,首先計算 array | myMap 的值:
newArrayForNgRepeat = [ { value: 2 }, { value: 3 }, { value: 4 }, { value: 5 }, ]
先比較 track by 的元素。用戶沒有指定,默認為 $id(item)。
$id 發現數組中有一些元素沒有 $$hashKey`,則給它們填充新 `$$hashKey,結果為
newArrayForNgRepeat = [ { value: 2, "$$hashKey": "object:5" }, { value: 3, "$$hashKey": "object:6" }, { value: 4, "$$hashKey": "object:7" }, { value: 5, "$$hashKey": "object:8" }, ]
這時兩邊的 track by 的實際結果為
"object:1" => "object:5"
"object:2" => "object:6"
"object:3" => "object:7"
"object:4" => "object:8"
兩者對不上,說明數組被做了增刪元素或者移動元素的操作。將其規整
"object:1" => undefined
"object:2" => undefined
"object:3" => undefined
"object:4" => undefined
undefined => "object:5"
undefined => "object:6"
undefined => "object:7"
undefined => "object:8"
結論是:
原數組全部 4 個元素被刪除
新添加了 4 個元素
于是 angular 把原來所有 li 刪除,再創建 4 個新的 li 元素,填充它們的 textContent,放到 ul 里
如果懷疑我說的話,請自己在瀏覽器里測試。你可以清楚的看到調試工具里 DOM 樹的閃爍
track by 與性能不恰當的 ng-repeat 會造成 DOM 樹反復重新構造,拖慢瀏覽器響應速度,造成頁面閃爍。除了上面這種比較極端的情況,如果一個列表頻繁拉取 Server 端數據自刷新的話也一定要手工添加 track by,因為接口給前端的數據是不可能包含 $$hashKey 這種東西的,于是結果就造成列表頻繁的重建。
其實不必考慮那么多,總之加上沒壞處,至少可以避免 angular 生成 $$hashKey 這種奇奇怪怪的東西。所以
請給 ng-repeat 手工添加 track by!
重要的事情再說一遍
請給 ng-repeat 手工添加 track by!
通常列表都是請求接口從數據庫中讀取返回的。通常數據庫中的記錄都有一個 id 字段做主鍵,那么這時使用 id 作為 track by 的字段是最佳選擇。如果沒有,可以選擇一些業務字段但是確保不會重復的。例如一個連表頭都是動態生成的表格,表頭就可以使用其字段名作為 track by 的字段(對象的 key 是不會重復的)。
如果真的找不到用于 track by 的字段,讓 angular 自動生成 $$hashKey 也不是不可以,但是切記檢查有沒有出現 DOM 元素不斷重刷的現象,除了仔細看調試工具的 DOM 樹是否閃爍之外,給列表中的元素添加一個特別的標記(比如 style="background: red"),也是一個行之有效的方法(如果這個標記被意外清除,說明原來的 DOM 元素被刪除了)。
除非真的沒辦法,不推薦使用 $index 作為 track by 的字段。
track by 與 單次綁定 連用track by 只是讓 angular 復用已有 DOM 元素。數組每個子元素內部綁定表達式的臟檢查還是免不了的。然而對于實際應用場景,往往是數組整體改變(例如分頁),數組每一項通常卻不會多帶帶變化。這時就可以通過使用單次綁定大量減少 $watch 表達式的數量。例如
除非 track by 字段改變造成的 DOM 樹重建,item.a 等一旦顯示在頁面上后就不會再被監視。
如果每行有 5 個綁定表達式,每頁顯示 20 條記錄,通過這種方法每頁就可以減少 5 * 20 = 100 個綁定表達式的監視。
注意:如果在 ng-repeat 內部使用的單次綁定,就一定不要用 track by $index。否則用戶切換下一頁頁面也不會更新。
使用分頁減少綁定個數這個就不多說了。能后端分頁的就后端分頁;接口不支持分頁的也要前端分頁;前端分頁時可以簡單的寫個 filter 用 Array.prototype.slice 實現。
能直接減少數組中項的個數就不要在 ng-repeat 中每項上寫 ng-show 或 ng-if
寫在最后的話臟檢查這個東西,其實在三大主流前端框架中或多或少都有涉及。React 每次生成新的 Virtual DOM,與舊 Virtual DOM 的 diff 操作本來就可以看做一次臟檢查。Vue 從相對徹底的拋棄了臟檢查機制,使用 Property 主動觸發 UI 更新,但是 Vue 仍然不能拋棄 track by 這個東西。
既然臟檢查在三大主流框架里或多或少都有所保留,為什么唯獨 Angular 的性能被廣為詬病呢?其實還是說在 Angular 1 的機制下,臟檢查的執行范圍過大以及頻率太過頻繁了。Angular 1.5 從 Angular 2+ 引入了組件(Component)的概念,然而形似而神非,其實只是一個特殊的 Directive 馬甲而已,并不能將臟檢查的執行范圍限制在各個組件之內,所以并不能本質的改變 Angular 1 臟檢查機制效率低下的現狀。
也許 Angular 1 終將被淘汰。但 Angular 作為前端第一個 MVVM 框架,著實引發了前端框架更新換代的熱潮。百足之蟲死而不僵,不管怎么樣我還得繼續維護停留在電腦里的 Angular 1 項目。不過也許老板哪天大發慈悲給我們用 Vue 重構整個項目的時間,將來的事情誰知道呢?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/51072.html
摘要:通常寫代碼時我們無需主動調用或是因為在外部對我們的回調函數做了包裝。類似的不只是這些事件回調函數,還有等。常量依舊會重復檢查。會檢查中有沒有一個名為的成員。 TL;DR 臟檢查是一種模型到視圖的數據映射機制,由 $apply 或 $digest 觸發。 臟檢查的范圍是整個頁面,不受區域或組件劃分影響 使用盡量簡單的綁定表達式提升臟檢查執行速度 盡量減少頁面上綁定表達式的個數(單次綁定...
摘要:本文針對的讀者具備性能優化的相關知識雅虎條性能優化原則高性能網站建設指南等擁有實戰經驗。這種機制能減少瀏覽器次數,從而提高性能。僅會檢查該和它的子,當你確定當前操作僅影響它們時,用可以稍微提升性能。 搬運自: http://atian25.github.io/2014/05/09/angular-performace/ 不知不覺,在項目中用angular已經半年多了,踩了很多坑...
摘要:我們再來看一下調用棧,如下圖從圖中我們發現了一個調用棧的代碼執行過,還記得里提到嗎發起臟檢查的通知者,它代理了原生事件,任何一個原生異步事件的觸發都會導致的運行。 尋找真兇Echarts or Angular 這是一篇故事,就如同技術,我們所追求的不是一個結局,而是那些深受啟發與共鳴的過程,那是我們成長的經驗與生產力的積淀! 故事開始于瘋了的ionic3應用 頁面打開,什么也沒做5s里...
摘要:來源于社區,時至今日已經基本成為的標配了。部分很簡單,要根據傳入的執行不同的操作。當性能遇到瓶頸時基本不會遇到,可以更改,保證傳入數據來提升性能。當不再能滿足程序開發的要求時,可以嘗試使用進行函數式編程。 Immutable & Redux in Angular Way 寫在前面 AngularJS 1.x版本作為上一代MVVM的框架取得了巨大的成功,現在一提到Angular,哪怕是已...
閱讀 2601·2021-11-15 11:38
閱讀 2618·2021-11-04 16:13
閱讀 17981·2021-09-22 15:07
閱讀 1014·2019-08-30 15:55
閱讀 3261·2019-08-30 14:15
閱讀 1663·2019-08-29 13:59
閱讀 3207·2019-08-28 18:28
閱讀 1575·2019-08-23 18:29