Angular JS (Angular.JS) 是一組用來開發Web頁面的架構、模板以及資料繫結和豐富UI組件。它支援整個開發進程,提供web應用的架構,無需進行手工DOM操作。 AngularJS很小,只有60K,相容主流瀏覽器,與 jQuery 配合良好。雙向資料繫結可能是AngularJS最酷最實用的特性,將MVC的原理展現地淋漓盡致.
AngularJS的工作原理是:HTML模板將會被瀏覽器解析到DOM中, DOM結構成為AngularJS編譯器的輸入。AngularJS將會遍曆DOM模板, 來產生相應的NG指令,所有的指令都負責針對view(即HTML中的ng-model)來設定資料繫結。因此, NG架構是在DOM載入完成之後, 才開始起作用的.
在html中:
<body ng-app="ngApp"> <div ng-controller="ngCtl"> <label ng-model="myLabel"></label> <input type="text" ng-model="myInput" /> <button ng-model="myButton" ng-click="btnClicked"></button> </div></body>
在js中:
// angular appvar app = angular.module("ngApp", [], function(){ console.log("ng-app : ngApp");});// angular controllerapp.controller("ngCtl", [ '$scope', function($scope){ console.log("ng-controller : ngCtl"); $scope.myLabel = "text for label"; $scope.myInput = "text for input"; $scope.btnClicked = function() { console.log("Label is " + $scope.myLabel); }}]);
如上,我們在html中先定義一個angular的app,指定一個angular的controller,則該controller會對應於一個範圍(可以用$scope首碼來指定範圍中的屬性和方法等). 則在該ngCtl的範圍內的HTML標籤, 其值或者操作都可以通過$scope的方式跟js中的屬性和方法進行綁定.
這樣, 就實現了NG的雙向資料繫結: 即HTML中呈現的view與AngularJS中的資料是一致的. 修改其一, 則對應的另一端也會相應地發生變化.
這樣的方式,使用起來真的非常方便. 我們僅關心HTML標籤的樣式, 及其對應在js中angular controller範圍下綁定的屬性和方法. 僅此而已, 將眾多複雜的DOM操作全都省略掉了.
這樣的思想,其實跟jQuery的DOM查詢和操作是完全不一樣的, 因此也有很多人建議用AngularJS的時候,不要混合使用jQuery. 當然, 二者各有優劣, 使用哪個就要看自己的選擇了.
NG中的app相當於一個模組module, 在每個app中可以定義多個controller, 每個controller都會有各自的範圍空間,不會相互幹擾.
綁定資料是怎樣生效的
初學AngularJS的人可能會踩到這樣的坑,假設有一個指令:
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不是雙向繫結嗎,為什麼資料變化了,介面沒有跟著重新整理?
試試在scope.counter++;這句之後加一句scope.digest();再看看是不是好了?
為什麼要這麼做呢,什麼情況下要這麼做呢?我們發現第一個例子中並沒有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元素上。問題就在於,怎樣觸發髒檢測?什麼時候觸發?
我們知道,一些基於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這樣把大量操作延時到一次更新,否則效能會更低。
所以說,兩種不同的監控方式,各有其優缺點,最好的辦法是瞭解各自使用方式的差異,考慮出它們效能的差異所在,在不同的業務情境中,避開最容易造成效能瓶頸的用法。