Angular JS (Angular.JS) 是一組用來(lái)開(kāi)發(fā)Web頁(yè)面的框架、模板以及數(shù)據(jù)綁定和豐富UI組件。它支持整個(gè)開(kāi)發(fā)進(jìn)程,提供web應(yīng)用的架構(gòu),無(wú)需進(jìn)行手工DOM操作。 AngularJS很小,只有60K,兼容主流瀏覽器,與 jQuery 配合良好。雙向數(shù)據(jù)綁定可能是AngularJS最酷最實(shí)用的特性,將MVC的原理展現(xiàn)地淋漓盡致.
AngularJS的工作原理是:HTML模板將會(huì)被瀏覽器解析到DOM中, DOM結(jié)構(gòu)成為AngularJS編譯器的輸入。AngularJS將會(huì)遍歷DOM模板, 來(lái)生成相應(yīng)的NG指令,所有的指令都負(fù)責(zé)針對(duì)view(即HTML中的ng-model)來(lái)設(shè)置數(shù)據(jù)綁定。因此, NG框架是在DOM加載完成之后, 才開(kāi)始起作用的.
在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 app
var app = angular.module("ngApp", [], function(){
console.log("ng-app : ngApp");
});
// angular controller
app.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);
}
}]);
如上,我們?cè)趆tml中先定義一個(gè)angular的app,指定一個(gè)angular的controller,則該controller會(huì)對(duì)應(yīng)于一個(gè)作用域(可以用$scope前綴來(lái)指定作用域中的屬性和方法等). 則在該ngCtl的作用域內(nèi)的HTML標(biāo)簽, 其值或者操作都可以通過(guò)$scope的方式跟js中的屬性和方法進(jìn)行綁定.
這樣, 就實(shí)現(xiàn)了NG的雙向數(shù)據(jù)綁定: 即HTML中呈現(xiàn)的view與AngularJS中的數(shù)據(jù)是一致的. 修改其一, 則對(duì)應(yīng)的另一端也會(huì)相應(yīng)地發(fā)生變化.
這樣的方式,使用起來(lái)真的非常方便. 我們僅關(guān)心HTML標(biāo)簽的樣式, 及其對(duì)應(yīng)在js中angular controller作用域下綁定的屬性和方法. 僅此而已, 將眾多復(fù)雜的DOM操作全都省略掉了.
這樣的思想,其實(shí)跟jQuery的DOM查詢和操作是完全不一樣的, 因此也有很多人建議用AngularJS的時(shí)候,不要混合使用jQuery. 當(dāng)然, 二者各有優(yōu)劣, 使用哪個(gè)就要看自己的選擇了.
NG中的app相當(dāng)于一個(gè)模塊module, 在每個(gè)app中可以定義多個(gè)controller, 每個(gè)controller都會(huì)有各自的作用域空間,不會(huì)相互干擾.
綁定數(shù)據(jù)是怎樣生效的
初學(xué)AngularJS的人可能會(huì)踩到這樣的坑,假設(shè)有一個(gè)指令:
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>
這個(gè)時(shí)候,點(diǎn)擊按鈕,界面上的數(shù)字并不會(huì)增加。很多人會(huì)感到迷惑,因?yàn)樗榭凑{(diào)試器,發(fā)現(xiàn)數(shù)據(jù)確實(shí)已經(jīng)增加了,Angular不是雙向綁定嗎,為什么數(shù)據(jù)變化了,界面沒(méi)有跟著刷新?
試試在scope.counter++;這句之后加一句scope.digest();再看看是不是好了?
為什么要這么做呢,什么情況下要這么做呢?我們發(fā)現(xiàn)第一個(gè)例子中并沒(méi)有digest,而且,如果你寫(xiě)了digest,它還會(huì)拋出異常,說(shuō)正在做其他的digest,這是怎么回事?
我們先想想,假如沒(méi)有AngularJS,我們想要自己實(shí)現(xiàn)這么個(gè)功能,應(yīng)該怎樣?
<!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">
/* 數(shù)據(jù)模型區(qū)開(kāi)始 */
var counter = 0;
function inc() {
counter++;
}
function inc2() {
counter+=2;
}
/* 數(shù)據(jù)模型區(qū)結(jié)束 */
/* 綁定關(guān)系區(qū)開(kāi)始 */
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;
}
}
/* 綁定關(guān)系區(qū)結(jié)束 */
</script>
</body>
</html>
可以看到,在這么一個(gè)簡(jiǎn)單的例子中,我們做了一些雙向綁定的事情。從兩個(gè)按鈕的點(diǎn)擊到數(shù)據(jù)的變更,這個(gè)很好理解,但我們沒(méi)有直接使用DOM的onclick方法,而是搞了一個(gè)ng-click,然后在bind里面把這個(gè)ng-click對(duì)應(yīng)的函數(shù)拿出來(lái),綁定到onclick的事件處理函數(shù)中。為什么要這樣呢?因?yàn)閿?shù)據(jù)雖然變更了,但是還沒(méi)有往界面上填充,我們需要在此做一些附加操作。
從另外一個(gè)方面看,當(dāng)數(shù)據(jù)變更的時(shí)候,需要把這個(gè)變更應(yīng)用到界面上,也就是那三個(gè)span里。但由于Angular使用的是臟檢測(cè),意味著當(dāng)改變數(shù)據(jù)之后,你自己要做一些事情來(lái)觸發(fā)臟檢測(cè),然后再應(yīng)用到這個(gè)數(shù)據(jù)對(duì)應(yīng)的DOM元素上。問(wèn)題就在于,怎樣觸發(fā)臟檢測(cè)?什么時(shí)候觸發(fā)?
我們知道,一些基于setter的框架,它可以在給數(shù)據(jù)設(shè)值的時(shí)候,對(duì)DOM元素上的綁定變量作重新賦值。臟檢測(cè)的機(jī)制沒(méi)有這個(gè)階段,它沒(méi)有任何途徑在數(shù)據(jù)變更之后立即得到通知,所以只能在每個(gè)事件入口中手動(dòng)調(diào)用apply(),把數(shù)據(jù)的變更應(yīng)用到界面上。在真正的Angular實(shí)現(xiàn)中,這里先進(jìn)行臟檢測(cè),確定數(shù)據(jù)有變化了,然后才對(duì)界面設(shè)值。
所以,我們?cè)趎g-click里面封裝真正的click,最重要的作用是為了在之后追加一次apply(),把數(shù)據(jù)的變更應(yīng)用到界面上去。
那么,為什么在ng-click里面調(diào)用$digest的話,會(huì)報(bào)錯(cuò)呢?因?yàn)锳ngular的設(shè)計(jì),同一時(shí)間只允許一個(gè)$digest運(yùn)行,而ng-click這種內(nèi)置指令已經(jīng)觸發(fā)了$digest,當(dāng)前的還沒(méi)有走完,所以就出錯(cuò)了。
$digest和$apply
在Angular中,有$apply和$digest兩個(gè)函數(shù),我們剛才是通過(guò)$digest來(lái)讓這個(gè)數(shù)據(jù)應(yīng)用到界面上。但這個(gè)時(shí)候,也可以不用$digest,而是使用$apply,效果是一樣的,那么,它們的差異是什么呢?
最直接的差異是,$apply可以帶參數(shù),它可以接受一個(gè)函數(shù),然后在應(yīng)用數(shù)據(jù)之后,調(diào)用這個(gè)函數(shù)。所以,一般在集成非Angular框架的代碼時(shí),可以把代碼寫(xiě)在這個(gè)里面調(diào)用。
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;
});
除此之外,還有別的區(qū)別嗎?
在簡(jiǎn)單的數(shù)據(jù)模型中,這兩者沒(méi)有本質(zhì)差別,但是當(dāng)有層次結(jié)構(gòu)的時(shí)候,就不一樣了。考慮到有兩層作用域,我們可以在父作用域上調(diào)用這兩個(gè)函數(shù),也可以在子作用域上調(diào)用,這個(gè)時(shí)候就能看到差別了。
對(duì)于$digest來(lái)說(shuō),在父作用域和子作用域上調(diào)用是有差別的,但是,對(duì)于$apply來(lái)說(shuō),這兩者一樣。我們來(lái)構(gòu)造一個(gè)特殊的示例:
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(); //這個(gè)換成$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>
這時(shí)候,我們就能看出差別了,在increase b按鈕上點(diǎn)擊,這時(shí)候,a跟b的值其實(shí)都已經(jīng)變化了,但是界面上的a沒(méi)有更新,直到點(diǎn)擊一次increase a,這時(shí)候剛才對(duì)a的累加才會(huì)一次更新上來(lái)。怎么解決這個(gè)問(wèn)題呢?只需在increaseb這個(gè)指令的實(shí)現(xiàn)中,把$digest換成$apply即可。
當(dāng)調(diào)用$digest的時(shí)候,只觸發(fā)當(dāng)前作用域和它的子作用域上的監(jiān)控,但是當(dāng)調(diào)用$apply的時(shí)候,會(huì)觸發(fā)作用域樹(shù)上的所有監(jiān)控。
因此,從性能上講,如果能確定自己作的這個(gè)數(shù)據(jù)變更所造成的影響范圍,應(yīng)當(dāng)盡量調(diào)用$digest,只有當(dāng)無(wú)法精確知道數(shù)據(jù)變更造成的影響范圍時(shí),才去用$apply,很暴力地遍歷整個(gè)作用域樹(shù),調(diào)用其中所有的監(jiān)控。
從另外一個(gè)角度,我們也可以看到,為什么調(diào)用外部框架的時(shí)候,是推薦放在$apply中,因?yàn)橹挥羞@個(gè)地方才是對(duì)所有數(shù)據(jù)變更都應(yīng)用的地方,如果用$digest,有可能臨時(shí)丟失數(shù)據(jù)變更。
臟檢測(cè)的利弊
很多人對(duì)Angular的臟檢測(cè)機(jī)制感到不屑,推崇基于setter,getter的觀測(cè)機(jī)制,在我看來(lái),這只是同一個(gè)事情的不同實(shí)現(xiàn)方式,并沒(méi)有誰(shuí)完全勝過(guò)誰(shuí),兩者是各有優(yōu)劣的。
大家都知道,在循環(huán)中批量添加DOM元素的時(shí)候,會(huì)推薦使用DocumentFragment,為什么呢,因?yàn)槿绻看味紝?duì)DOM產(chǎn)生變更,它都要修改DOM樹(shù)的結(jié)構(gòu),性能影響大,如果我們能先在文檔碎片中把DOM結(jié)構(gòu)創(chuàng)建好,然后整體添加到主文檔中,這個(gè)DOM樹(shù)的變更就會(huì)一次完成,性能會(huì)提高很多。
同理,在Angular框架里,考慮到這樣的場(chǎng)景:
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++;
}
};
}
如果界面上某個(gè)文本綁定這個(gè)numOfCheckedItems,會(huì)怎樣?在臟檢測(cè)的機(jī)制下,這個(gè)過(guò)程毫無(wú)壓力,一次做完所有數(shù)據(jù)變更,然后整體應(yīng)用到界面上。這時(shí)候,基于setter的機(jī)制就慘了,除非它也是像Angular這樣把批量操作延時(shí)到一次更新,否則性能會(huì)更低。
所以說(shuō),兩種不同的監(jiān)控方式,各有其優(yōu)缺點(diǎn),最好的辦法是了解各自使用方式的差異,考慮出它們性能的差異所在,在不同的業(yè)務(wù)場(chǎng)景中,避開(kāi)最容易造成性能瓶頸的用法。