1. 程式人生 > >ng-repeat中DOM的重新渲染機制

ng-repeat中DOM的重新渲染機制

引子:專案中遇到一個BUG,使用ui-bootstrap-tpls中的carousel外掛設定輪播圖時,改變ng-repeat中陣列其中一個元素時,輪播圖的順序發生了變化。
            程式碼如下: 

<div carousel interval="app.Interval.value" no-pasue="false" no-transition="true">
    <div slide ng-repeat="Item in advertiseList">
        <div ng-style="{'background-color': Item.BgColor}">
            <div class="slide">
                <img class="aside cursor-pointer" ng-src="{{Item.ImgUrl|ImgUrl}}">
            </div>
        </div>
    </div>
</div>
function changeAd() {
        angular.forEach($scope.advertiselList, function (item, index) {

        if (item.Id == $scope.editAd.Id) {
           $scope.advertiseList[index] = $scope.editAd;
        }
    });
}
通過檢視carsousel與slide組合指令的原始碼發現,當slide的DOM被重新渲染時,slide指令會在其銷燬時先將其移除,然後渲染時push到尾部。
在不想改變指令的情況下,這個問題只能從阻止ng-repeat對DOM的重新渲染著手。
正文:關於ng-repeat的重新渲染機制

如果ng-repeat陣列的每個item都是一個物件,ng-repeat會為每個item都生成一個$$hashKey

,用於繫結DOM。

情況一:通過track by傳入標識屬性,那麼ng-repeat會把此標識屬性繫結到DOM,並無視掉$$hashKey
解決方案:因此,這個問題可以簡單的通過將ng-repeat="Item in advertiseList"改變為ng-repeat="Item in advertiseList track by $index(track by item.Id)"來解決。

在這種語法下,$$hashKey不起作用的,只要item.Id相等就會認為兩個物件相同。

情況二:當沒有通過track by傳入標識屬性的情況下,預設是通過$$hashKey來繫結DOM。而$$hashKey
的改變與否,是通過$watchCollection對陣列進行監聽,當監聽到有變化時,會改變$$hashKey的值並重新渲染DOM。
這裡補充一下$watchCollection與$watch(obj,listener)、$watch(obj,listner,true)的區別
$watch(obj,listener)為引用比對,即只比對物件或陣列的引用,物件中單個屬性變化或者陣列中元素的變化並不會觸發listener函式,如果將物件或陣列想象成樹,那麼$watch為只比對根節點。
$watch(obj,listner,true)為值比對,$watch的第三個入參含義為是否值比對(或者全比對更好理解一點)即比對整個物件或陣列所有的值,如果陣列的元素是物件,會繼續比對物件元素的所有屬性,以此類推。可以理解成物件樹的全遍歷。
第一種方案策略太過簡單,第二種策略又太消耗效能,所以誕生了一個折中的淺層監聽策略。
$watchCollection是基於$watch監聽的淺層監聽函式,即對於陣列來說,監聽每個元素的引用,對於物件來說,監聽每個屬性的引用。可以理解為只遍歷樹的根節點和第一層元素。
解決方案:針對淺層監聽的機制,我們可以通過不改變陣列中item引用的方法來騙過監聽。
$scope.advertiseList[index] = $scope.editAd;
前面這句程式碼只是簡單的將$scope.advertiselList[index]的引用指向了一個新的地址,因此可以被檢測到,可以將這句話改成
angular.copy($scope.editAd,$scope.advertiseList)
angular中的copy入參傳入目標物件,會將源物件的所有屬性一一賦值給目標物件,不會改變目標物件的引用。(詳情請看另一篇angular中的深淺拷貝,如果“連結”點不動,說明還沒寫)。