6、表示式
在模板中使用表示式是為了以充分的靈活性在模板、業務邏輯和資料之間建立聯絡,同時又能避免讓業務邏輯滲透到模板中。
1 <div ng-controller="SomeController"> 2 <div>{{recompute() / 10}}</div> 3 <ul ng-repeat="thing in things"> 4 <li ng-class="{highlight: $index % 4 >= threshold($index)}"> 5 {{otherFunction($index)}} 6 </li> 7 </ul> 8 </div>
當然,對於第一個表示式,recompute() / 10,應該避免這種把業務邏輯放到模板中的方式,應該清晰地區分檢視和控制器之間的職責,這樣更便於測試。
雖然 Angular 裡面的表示式比 Javascript 更嚴格,但是它們對 undefined 和 null 的容錯性更好。如果遇到錯誤,模板只是簡單地什麼都不顯示,而不會丟擲一個 NullPointerException 錯誤,這樣你就可以安全地使用未經初始化的模型值,而一旦它們被賦值以後就會立即顯示出來。
7、區分 UI 和控制器的職責
在應用中控制器有三種職責:
- 為應用中的模型設定初始狀態
- 通過 $scope 物件把資料模型和函式暴露給檢視(UI模板)
- 監視模型其餘部分的變化,並採取相應的動作
建議為檢視中的每一塊功能區域建立一個控制器,這樣可以讓控制器保持小巧和可管理的狀態。
更為複雜的時候,你可以建立巢狀的控制器,通過內部的原型繼承機制,父控制器物件上的 $scope 會被傳遞給內部巢狀控制器的 $scope。
1 <div ng-controller="ParentController"> 2 <div ng-controller="ChildController">...</div> 3 </div>
上面的例子中,ChildController 的 $scope 物件可以訪問 ParentController 的 $scope 物件上的所有屬性(和函式)。
8、使用 $watch 監控資料模型的變化
在 scope 內建的所有函式中,用到最多的可能就是 $watch 函式了,當你的資料模型中的某一部分發生變化時,$watch 可以向你發出通知,監控單個物件的屬性,也可以監控需要經過計算的結果,只要能被當作屬性訪問到,或者可以當作一個 JavaScript 函式被計算出來,就可以被 $watch 函式監控。函式簽名為:
$watch(watchFn, watchAction, deepWatch)
watchFn —— 該引數是一個帶有 Angular 表示式或者函式的字串,返回被監控的資料模型的當前值。它會被執行多次,所以要保證不會產生副作用;
watchAction —— 通常以函式的形式,接收到 watchFn 的新舊兩個值,以及作用域物件的引用,函式簽名為 function(newValue, oldValue, scope)
deepWatch —— 如果設定為 true,則會去檢查被監控物件的每個屬性是否發生了變化,一般監控一個陣列或者多個物件時要設定為 true,但由於要遍歷陣列,所以運算負擔會比較重。
$watch 函式會返回一個屬性,當你不再需要接受變更通知時,可以用這個返回的函式登出監控器。如果需要監控一個屬性,然後登出監控,可以如下:
1 ... 2 var dereg = $scope.$watch('someModel.someProperty', callbackOnChange()); 3 ... 4 dereg();
一個購物車的例子,當使用者新增到購物車中的商品價格超過 100 美元的時候,會有 10 美元的折扣。使用下面的模板:
1 <html ng-app="shoppingCart"> 2 <body> 3 <div ng-controller="CartController"> 4 <div ng-repeat="item in items"> 5 <span>{{item.title}}</span> 6 <input ng-model="item.quantity"> 7 <span>{{item.price | currency}}</span> 8 <span>{{item.price * item.quantity | currency}}</span> 9 </div> 10 <div>Total: {{totalCart() | currency}}</div> 11 <div>Discount: {{bill.discount | currency}}</div> 12 <div>Subtotal: {{subtotal() | currency}}</div> 13 </div> 14 <script src="src/angular.js"></script> 15 <script src="shoppingCart.js"></script> 16 </body> 17 </html>
控制器如下:
1 var shoppingCart = angular.module('shoppingCart', []); 2 3 shoppingCart.controller('CartController', function ($scope) { 4 $scope.bill = {}; 5 6 $scope.items = [ 7 {title: 'Paint pots', quantity: 8, price: 3.95}, 8 {title: 'Polka dots', quantity: 17, price: 12.95}, 9 {title: 'Pebbles', quantity: 5, price: 6.95} 10 ]; 11 12 $scope.totalCart = function () { 13 var total = 0; 14 for (var i = 0, len = $scope.items.length; i < len; i++) { 15 total = total + $scope.items[i].price * $scope.items[i].quantity; 16 } 17 return total; 18 }; 19 20 function calculateDiscount(newValue, oldValue, scoope) { 21 $scope.bill.discount = newValue > 100 ? 10 : 0; 22 } 23 24 $scope.subtotal = function () { 25 return $scope.totalCart() - $scope.bill.discount; 26 }; 27 28 $scope.$watch($scope.totalCart, calculateDiscount); 29 });
使用者看到的效果如圖:
9、watch() 中的效能注意事項
斷點除錯 totalCart() 的程式碼,你會發現渲染這個頁面時,該函式被呼叫了 6 次。其中 3 次發生在每次呼叫它的時候:
- 模板 {{totalCart() | currency}}
- subtotal() 函式
- $watch() 函式
然後 Angular 又把整個過程重複了一遍,這樣的目的是:檢測模型中的變更已經被完整地進行了傳播,並且模型已經被設定好。Angular 的做法是,把所有被監控的屬性都拷貝一份,然後把它們和當前的值進行比較,看看是否發生了變化。實際上,Angular 可能執行上面過程不止兩遍,如果重複 10 遍發現屬性還在變化,Angular 會報錯並退出,這時候你需要解決迴圈依賴的問題了。
PS: 書裡面說的是,由於 Angular 需要用 JavaScript 實現資料繫結,官方開發團隊與 TC39 團隊一起開發了一個叫做 Object.observe() 的底層本地化實現,這樣可以讓你的資料繫結操作就像本地化程式碼一樣快速。
我們可以改成監控 items 陣列的變化,然後重新計算 $scope 屬性中的總價、折扣和小計值來減少函式呼叫次數。
模板修改如下:
1 <div>Total: {{totalCart() | currency}}</div> 2 <div>Discount: {{bill.discount | currency}}</div> 3 <div>Subtotal: {{subtotal() | currency}}</div>
控制器修改如下:
1 var shoppingCart = angular.module('shoppingCart', []); 2 3 shoppingCart.controller('CartController', function ($scope) { 4 $scope.bill = {}; 5 6 $scope.items = [ 7 {title: 'Paint pots', quantity: 8, price: 3.95}, 8 {title: 'Polka dots', quantity: 17, price: 12.95}, 9 {title: 'Pebbles', quantity: 5, price: 6.95} 10 ]; 11 12 function calculateDiscount(newValue, oldValue, scoope) { 13 var total = 0; 14 for (var i = 0, len = $scope.items.length; i < len; i++) { 15 total = total + $scope.items[i].price * $scope.items[i].quantity; 16 } 17 $scope.bill.totalCart = total; 18 $scope.bill.discount = newValue > 100 ? 10 : 0; 19 $scope.bill.subtotal = total - $scope.bill.discount; 20 } 21 22 $scope.$watch('items', calculateDiscount, true); 23 });
在呼叫 $watch 函式時把 items 寫成了一個字串,並且第三個引數設定為 true,可以監控整個 items 陣列的變化,單需要製作一份陣列的拷貝,用來進行比較操作。
所以,如果每次在 Angular 顯示頁面時只需要重新計算 bill 屬性,那麼效能會好很多。可以像下面這樣重新計算屬性值:
1 $scope.$watch(function (newValue, oldValue, scope) { 2 var total = 0; 3 for (var i = 0, len = $scope.items.length; i < len; i++) { 4 total = total + $scope.items[i].price * $scope.items[i].quantity; 5 } 6 $scope.bill.totalCart = total; 7 $scope.bill.discount = newValue > 100 ? 10 : 0; 8 $scope.bill.subtotal = total - $scope.bill.discount; 9 });
根據效能分析更是說明了這點(左邊是第二種方式):
當然可以使用上面說過的方法,利用 $watch 返回的函式,移除不必要的 $watch,參見破狼大神的例子:Angular 移除不必要的 $watch
10、監控多個東西
監控多個東西,有兩種基本的選擇:
- 監控把這些屬性連線起來之後的值
- 把它們放到一個陣列或者物件中,然後給 deepWatch 引數傳遞一個 true 值。
第二種情況上面已經介紹,第一種情況比較簡單,比如在你的作用域中存在一個 things 物件,它帶有兩個屬性 a 和 b,當這兩個屬性發生變化時都需要執行 callMe() 函式,你可以同時監控著兩個屬性, 示例如下:
$scope.$watch('things.a + things.b', callMe(...));
當然,a 和 b 也可以屬於不同的物件,這個列表可以很長,不過如果需要監控的屬性比較多, 不妨把這個列表放入一個函式中,返回連線的值。