Angular數據雙向綁定機制

數據的雙向綁定

Angular實現了雙向綁定機制。所謂的雙向綁定,無非是從界面的操做能實時反映到數據,數據的變動能實時展示到界面。javascript

一個最簡單的示例就是這樣:html

<div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter=counter+1">increase</button> </div>
function CounterCtrl($scope) { $scope.counter = 1; }

這個例子很簡單,毫無特別之處,每當點擊一次按鈕,界面上的數字就增長一。java

綁定數據是怎樣生效的

初學AngularJS的人可能會踩到這樣的坑,假設有一個指令:git

var app = angular.module("test", []); app.directive("myclick", function() { return function (scope, element, attr) { element.on("click", function() { scope.counter++; }); }; }); app.controller("CounterCtrl", function($scope) { $scope.counter = 0; });
<body ng-app="test"> <div ng-controller="CounterCtrl"> <button myclick>increase</button> <span ng-bind="counter"></span> </div> </body>

這個時候,點擊按鈕,界面上的數字並不會增長。不少人會感到迷惑,由於他查看調試器,發現數據確實已經增長了,Angular不是雙向綁定嗎,爲何數據變化了,界面沒有跟着刷新?github

試試在scope.counter++;這句以後加一句scope.digest();再看看是否是好了?app

爲何要這麼作呢,什麼狀況下要這麼作呢?咱們發現第一個例子中並無digest,並且,若是你寫了digest,它還會拋出異常,說正在作其餘的digest,這是怎麼回事?框架

咱們先想一想,假如沒有AngularJS,咱們想要本身實現這麼個功能,應該怎樣?函數

<!DOCTYPE html>
<html>
    <head> <meta charset="utf-8" /> <title>two-way binding</title> </head> <body onload="init()"> <button ng-click="inc"> increase 1 </button> <button ng-click="inc2"> increase 2 </button> <span style="color:red" ng-bind="counter"></span> <span style="color:blue" ng-bind="counter"></span> <span style="color:green" ng-bind="counter"></span>  <script type="text/javascript">  /* 數據模型區開始 */  var counter = 0;   function inc() {  counter++;  }   function inc2() {  counter+=2;  }  /* 數據模型區結束 */   /* 綁定關係區開始 */  function init() {  bind();  }   function bind() {  var list = document.querySelectorAll("[ng-click]");  for (var i=0; i<list.length; i++) {  list[i].onclick = (function(index) {  return function() {  window[list[index].getAttribute("ng-click")]();  apply();  };  })(i);  }  }   function apply() {  var list = document.querySelectorAll("[ng-bind='counter']");  for (var i=0; i<list.length; i++) {  list[i].innerHTML = counter;  }  }  /* 綁定關係區結束 */  </script> </body> </html>

能夠看到,在這麼一個簡單的例子中,咱們作了一些雙向綁定的事情。從兩個按鈕的點擊到數據的變動,這個很好理解,但咱們沒有直接使用DOM的onclick方法,而是搞了一個ng-click,而後在bind裏面把這個ng-click對應的函數拿出來,綁定到onclick的事件處理函數中。爲何要這樣呢?由於數據雖然變動了,可是尚未往界面上填充,咱們須要在此作一些附加操做。性能

從另一個方面看,當數據變動的時候,須要把這個變動應用到界面上,也就是那三個span裏。但因爲Angular使用的是髒檢測,意味着當改變數據以後,你本身要作一些事情來觸發髒檢測,而後再應用到這個數據對應的DOM元素上。問題就在於,怎樣觸發髒檢測?何時觸發?spa

咱們知道,一些基於setter的框架,它能夠在給數據設值的時候,對DOM元素上的綁定變量做從新賦值。髒檢測的機制沒有這個階段,它沒有任何途徑在數據變動以後當即獲得通知,因此只能在每一個事件入口中手動調用apply(),把數據的變動應用到界面上。在真正的Angular實現中,這裏先進行髒檢測,肯定數據有變化了,而後纔對界面設值。

因此,咱們在ng-click裏面封裝真正的click,最重要的做用是爲了在以後追加一次apply(),把數據的變動應用到界面上去。

那麼,爲何在ng-click裏面調用$digest的話,會報錯呢?由於Angular的設計,同一時間只容許一個$digest運行,而ng-click這種內置指令已經觸發了$digest,當前的尚未走完,因此就出錯了。

$digest和$apply

在Angular中,有$apply和$digest兩個函數,咱們剛纔是經過$digest來讓這個數據應用到界面上。但這個時候,也能夠不用$digest,而是使用$apply,效果是同樣的,那麼,它們的差別是什麼呢?

最直接的差別是,$apply能夠帶參數,它能夠接受一個函數,而後在應用數據以後,調用這個函數。因此,通常在集成非Angular框架的代碼時,能夠把代碼寫在這個裏面調用。

var app = angular.module("test", []); app.directive("myclick", function() { return function (scope, element, attr) { element.on("click", function() { scope.counter++; scope.$apply(function() { scope.counter++; }); }); }; }); app.controller("CounterCtrl", function($scope) { $scope.counter = 0; });

除此以外,還有別的區別嗎?

在簡單的數據模型中,這二者沒有本質差異,可是當有層次結構的時候,就不同了。考慮到有兩層做用域,咱們能夠在父做用域上調用這兩個函數,也能夠在子做用域上調用,這個時候就能看到差異了。

對於$digest來講,在父做用域和子做用域上調用是有差異的,可是,對於$apply來講,這二者同樣。咱們來構造一個特殊的示例:

var app = angular.module("test", []); app.directive("increasea", function() { return function (scope, element, attr) { element.on("click", function() { scope.a++; scope.$digest(); }); }; }); app.directive("increaseb", function() { return function (scope, element, attr) { element.on("click", function() { scope.b++; scope.$digest(); //這個換成$apply便可 }); }; }); app.controller("OuterCtrl", ["$scope", function($scope) { $scope.a = 1; $scope.$watch("a", function(newVal) { console.log("a:" + newVal); }); $scope.$on("test", function(evt) { $scope.a++; }); }]); app.controller("InnerCtrl", ["$scope", function($scope) { $scope.b = 2; $scope.$watch("b", function(newVal) { console.log("b:" + newVal); $scope.$emit("test", newVal); }); }]);
<div ng-app="test"> <div ng-controller="OuterCtrl"> <div ng-controller="InnerCtrl"> <button increaseb>increase b</button> <span ng-bind="b"></span> </div> <button increasea>increase a</button> <span ng-bind="a"></span> </div> </div> 

這時候,咱們就能看出差異了,在increase b按鈕上點擊,這時候,a跟b的值其實都已經變化了,可是界面上的a沒有更新,直到點擊一次increase a,這時候剛纔對a的累加纔會一次更新上來。怎麼解決這個問題呢?只需在increaseb這個指令的實現中,把$digest換成$apply便可。

當調用$digest的時候,只觸發當前做用域和它的子做用域上的監控,可是當調用$apply的時候,會觸發做用域樹上的全部監控。

所以,從性能上講,若是能肯定本身做的這個數據變動所形成的影響範圍,應當儘可能調用$digest,只有當沒法精確知道數據變動形成的影響範圍時,纔去用$apply,很暴力地遍歷整個做用域樹,調用其中全部的監控。

從另一個角度,咱們也能夠看到,爲何調用外部框架的時候,是推薦放在$apply中,由於只有這個地方纔是對全部數據變動都應用的地方,若是用$digest,有可能臨時丟失數據變動。

髒檢測的利弊

不少人對Angular的髒檢測機制感到不屑,推崇基於setter,getter的觀測機制,在我看來,這只是同一個事情的不一樣實現方式,並無誰徹底賽過誰,二者是各有優劣的。

你們都知道,在循環中批量添加DOM元素的時候,會推薦使用DocumentFragment,爲何呢,由於若是每次都對DOM產生變動,它都要修改DOM樹的結構,性能影響大,若是咱們能先在文檔碎片中把DOM結構建立好,而後總體添加到主文檔中,這個DOM樹的變動就會一次完成,性能會提升不少。

同理,在Angular框架裏,考慮到這樣的場景:

function TestCtrl($scope) { $scope.numOfCheckedItems = 0; var list = []; for (var i=0; i<10000; i++) { list.push({ index: i, checked: false }); } $scope.list = list; $scope.toggleChecked = function(flag) { for (var i=0; i<list.length; i++) { list[i].checked = flag; $scope.numOfCheckedItems++; } }; }

若是界面上某個文本綁定這個numOfCheckedItems,會怎樣?在髒檢測的機制下,這個過程毫無壓力,一次作完全部數據變動,而後總體應用到界面上。這時候,基於setter的機制就慘了,除非它也是像Angular這樣把批量操做延時到一次更新,不然性能會更低。

因此說,兩種不一樣的監控方式,各有其優缺點,最好的辦法是瞭解各自使用方式的差別,考慮出它們性能的差別所在,在不一樣的業務場景中,避開最容易形成性能瓶頸的用法。

轉載自:Angular沉思錄(一)數據綁定 #10

相關文章
相關標籤/搜索