$apply
或 $digest
觸發。ng-if
)ng-repeat
添加 track by
讓 angular 複用已有元素Angular 是一個 MVVM 前端框架,提供了雙向數據綁定。所謂雙向數據綁定(Two-way data binding)就是頁面元素變化會觸發 View-model 中對應數據改變,反過來 View-model 中數據變化也會引起所綁定的 UI 元素數據更新。操做數據就等同於操做 UI。html
看似簡單,其實水很深。UI 元素變化引起 Model 中數據變化這個經過綁定對應 DOM 事件(例如 input
或 change
)能夠簡單的實現;然而反過來就不是那麼容易。前端
好比有以下代碼:程序員
<p ng-bind="content1"></p> <p ng-bind="content2"></p> <button ng-click="onClick()">Click Me</button>
用戶點擊了 button,angular 執行了一個叫 onClick
的方法。這個 onClick
的方法體對於 angular 來講是黑盒,它到底作了什麼不知道。可能改了 $scope.content1
的值,可能改了 $scope.content2
的值,也可能兩個值都改了,也可能都沒改。angularjs
那麼 angular 到底應該怎樣得知 onClick()
這段代碼後是否應該刷新 UI,應該更新哪一個 DOM 元素?算法
angular 必須去挨個檢查這些元素對應綁定表達式的值是否有被改變。這就是髒數據檢查的由來(髒數據檢查如下簡稱髒檢查)。數據庫
angular 會在可能觸發 UI 變動的時候進行髒檢查:這句話並不許確。實際上,髒檢查是 $digest](https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$digest) 執行的,另外一個更經常使用的用於觸發髒檢查的函數 [$apply 其實就是 $digest
的一個簡單封裝(還作了一些抓異常的工做)。express
一般寫代碼時咱們無需主動調用 $apply
或 $digest
是由於 angular 在外部對咱們的回調函數作了包裝。例如經常使用的 ng-click
,這是一個指令(Directive
),內部實現則 相似 於json
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
的表達式是否跟觸發髒檢查的事件有關。api
例如:
<div ng-show="false"> <span id="span1" ng-bind="content"></span> </div> <span id="span2" ng-bind="content"></span> <button ng-click="">TEST</button>
問:點擊 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 裏都會遍歷而且生成新數組)單次綁定(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 選項卡實現:
<ul> <li ng-class="{ selected: selectedTab === 1 }">Tab 1 title</li> <li ng-class="{ selected: selectedTab === 1 }">Tab 2 title</li> <li ng-class="{ selected: selectedTab === 1 }">Tab 3 title</li> <li ng-class="{ selected: selectedTab === 1 }">Tab 4 title</li> </ul> <div ng-show="selectedTab === 1">[[Tab 1 body...]]</div> <div ng-show="selectedTab === 2">[[Tab 2 body...]]</div> <div ng-show="selectedTab === 3">[[Tab 3 body...]]</div> <div ng-show="selectedTab === 4">[[Tab 4 body...]]</div>
對於這種會反覆隱藏、顯示的元素,一般人們第一反應都是使用 ng-show
或 ng-hide
簡單的用 display: none
把元素設置爲不可見。
然而入上文所說,肉眼不可見不表明不會跑髒檢查。若是將 ng-show
替換爲 ng-if
或 ng-switch-when
<div ng-if="selectedTab === 1">[[Tab 1 body...]]</div> <div ng-if="selectedTab === 2">[[Tab 2 body...]]</div> <div ng-if="selectedTab === 3">[[Tab 3 body...]]</div> <div ng-if="selectedTab === 4">[[Tab 4 body...]]</div>
有以下優勢:
$watch
表達式也減小至四分之一,提高髒檢查循環的速度controller
(例如每一個 tab 都被封裝爲一個組件),那麼僅當這個 tab 被選中時該 controller
纔會執行,能夠減小各頁面的互相干擾controller
中調用接口獲取數據,那麼僅當對應 tab
被選中時纔會加載,避免網絡擁擠固然也有缺點:
controller
,那麼每次該 tab 被選中時 controller
都會被執行controller
裏面調接口獲取數據,那麼每次該 tab 被選中時都會從新加載各位讀者本身取捨。
ng-repeat
!這就更有(e)趣(xin)了。一般的綁定只是去監聽一個值的變化(綁定對象也是綁定到對象裏的某個成員),而 ng-repeat
卻要監視一整個數組對象的變化。例若有:
<ul ng-init="array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, ]"> <li ng-repeat="item in array" ng-bind="item.value"></li> </ul>
會生成 4 個 li
元素
沒有問題。若是我添加一個按鈕以下:
<button ng-click="array.shift()">刪除第一個元素</button>
請考慮:當用戶點擊這個按鈕會發生什麼?
咱們一步一步分析。開始的時候,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 通過比較,看到的是:
反應到 DOM 元素上就是:
li
內容改成 2li
內容改成 3li
內容改成 4li
刪掉能夠看到,刪除一個元素致使了整個 ul
序列的刷新。要知道 DOM 操做要比 JS 變量操做要慢得多,相似這樣的無用操做最好能想辦法避免。
那麼問題出在哪裏呢?用戶刪除了數組的第一個元素,致使了整個數組元素前移;然而 angular 無法得知用戶作了這樣一個刪除操做,只能傻傻的按下標一個一個比。
那麼只要引入一種機制來標記數組的每一項就行了吧。因而 angular 引入了 track by
用來標記數組元素的必定是數組裏相似 ID 的某個值。這個值必定要符合如下這兩個特色。
基於這兩個特色。若是用戶沒有給 ng-repeat
指定 track by
的表達式,則默認爲內置函數 $id。$id
會檢查 item
中有沒有一個名爲 $$hashKey` 的成員。若有,返回其值;如沒有,則生成一個新的惟一值寫入。這就是數組中那個奇怪的 `$$hashKey
成員來歷,默認值是 "object:X"
(你問我爲何是個字符串而不是數字?我怎麼知道。。。)
仍是前面的問題,引入 track by
後再來看。由於沒有指定 track by
,則默認爲 $id(item)
,實際爲 $$hashKey
。
<ul ng-init="array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, ]"> <li ng-repeat="item in array track by $id(item)" ng-bind="item.value"></li> </ul>
開始的時候,$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
的用處。
So far so good! 然而需求某天有變,程序員小哥決定用 filter 給數組作 map 後再渲染。
<ul ng-init="array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }, ]"> <li ng-repeat="item in array | myMap" ng-bind="item.value"></li> </ul>
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:
這裏請再次注意:數組 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"object:5"
"object:6"
"object:7"
"object:8"
結論是:
因而 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
表達式的數量。例如
<li ng-repeat="item in array track by item.id"> <div>a: <span ng-bind="::item.a"></span></div> <div>b: <span ng-bind="::item.b"></span></div> <div>c: <span ng-bind="::item.c"></span></div> <div>d: <span ng-bind="::item.d"></span></div> <div>e: <span ng-bind="::item.e"></span></div> </li>
除非 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 重構整個項目的時間,未來的事情誰知道呢?