AngularJS 的常用特性(三)

shaopiing發表於2016-05-22

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 也可以屬於不同的物件,這個列表可以很長,不過如果需要監控的屬性比較多, 不妨把這個列表放入一個函式中,返回連線的值。

特別感謝《用 AngularJS 開發下一代 Web 應用》

相關文章