數(shù)據(jù)綁定可以說是AngularJS最大的特色,。在Angular中,,視圖和模型的數(shù)據(jù)不僅是雙向綁定的,并而且是實(shí)時(shí)的,。
使用Angular可以做到良好的甚至是神奇的用戶體驗(yàn),,例如用戶在輸入表單的過程中實(shí)時(shí)地提示輸入有誤或者輸入正確。
雙向綁定
下圖是模板引擎中常見的單向數(shù)據(jù)綁定:
通常在服務(wù)器端,,將數(shù)據(jù)模型和模板結(jié)合,,生成視圖。當(dāng)視圖中的數(shù)據(jù)發(fā)生改變時(shí),,數(shù)據(jù)模型不會自動更新,;模型發(fā)生改變時(shí),視圖也不會自動刷新,。
因此開發(fā)者不得不寫大量的代碼來同步視圖和模型,。例如:
- 視圖->模型:綁定DOM事件來監(jiān)聽視圖的改變,進(jìn)而通過javascript函數(shù)來同步數(shù)據(jù)模型,,更改javascript對象,,或者發(fā)送HTTP請求到后臺。
- 模型->視圖:模型改變時(shí)通過jQuery操作來更新DOM,。如果數(shù)據(jù)模型在后臺,,可能還需要websocket之類的推送機(jī)制。
而Angular提供了雙向的數(shù)據(jù)綁定,,我們可以在Angular Controller的$scope 中聲明數(shù)據(jù)模型,,在模板中進(jìn)行綁定。
Angular會自動添加DOM事件,,并在$scope 發(fā)生改變時(shí)自動進(jìn)行DOM操作,。下面是Angular雙向綁定的MVT關(guān)系示意圖:
圖片來源: https://docs./guide/databinding
Scope
scope 在Angular中代表著應(yīng)用模型,它是模板中表達(dá)式的上下文,。在scope 中,你可以watch (監(jiān)聽)表達(dá)式值的變化,,可以傳播事件,。
在編寫控制器時(shí),我們往往會注入一個(gè)$scope Service來設(shè)置當(dāng)前模板的上下文:
var app = angular.module('helloWorldApp', []);
app.controller('worldCtrl', ['$scope', '$http', function($scope, $http) {
// 在模板上下文中添加一個(gè)變量:username
$scope.username = 'harttle';
}]);
然后在視圖中綁定它:
<div ng-app='helloWorldApp' ng-controller='worldCtrl'>
<input type='text' ng-model='username'>
<span ng-bind='username'>
</div>
ng-model 設(shè)置了input 內(nèi)容和當(dāng)前上下文中username 之間的雙向綁定,;ng-bind 設(shè)置了從上下文到span 內(nèi)容的綁定,。
當(dāng)我們在input 中輸入時(shí),,span 的內(nèi)容便會實(shí)時(shí)地改變。
更多$scope 的信息請參考: AngularJS 初始化過程
官方文檔: https://docs./api/ng/type/$rootScope.Scope
Scope通信
因?yàn)锳ngular的Controller可以嵌套,,子controller的$scope 中可以直接訪問父$scope 中的屬性,。
子$scope 中可以通過$emit 方法來發(fā)射一個(gè)事件,父$scope 中通過$on 來監(jiān)聽該事件:
// 子Controller中發(fā)射的事件會驗(yàn)證Controller的層級逐級上傳
// 第一個(gè)參數(shù)為事件名,,后面是任意個(gè)數(shù)的參數(shù)
$scope.$emit('alienDestroyed', args, ...)
// 父Controller中通過事件名來監(jiān)聽
$scope.$on('alienDestroyed', function(event, args, ...){});
如果兩個(gè)控制器并非父子關(guān)系,還可以通過$broadcast 方法來發(fā)送事件,。
Angular雖然提供了Scope之間的通信機(jī)制,,但濫用事件和通知將會使得你的控制器難以理解和維護(hù)。
如果Controller間有數(shù)據(jù)共享,,把數(shù)據(jù)抽取為Service更加合適,。
$watch
在scope 中,有時(shí)我們希望監(jiān)聽某個(gè)表達(dá)式的變化,。在Angular的scope 中,,監(jiān)聽表達(dá)式的值就像注冊事件處理函數(shù)一樣簡單:
$scope.$watch('username', function(newValue, oldValue){
console.log('username changed:', oldValue, '->', newValue);
});
是不是很神奇?但是你可能會發(fā)現(xiàn)有時(shí)$watch 并不起作用,,這時(shí)你可能需要對Angular Scope中$watch 的策略有更多的了解,。Scope有三個(gè)監(jiān)聽方法:
$watch :通過引用(reference)監(jiān)聽,這時(shí)最高效的策略,。只有該表達(dá)式返回值的引用(類似C++的地址)發(fā)生變化時(shí)才會觸發(fā),。
$watchCollection :監(jiān)聽數(shù)組或?qū)ο蟮脑兀ㄒ茫┑淖兓?/li>
$watch(Expression, listener, true) :監(jiān)聽值的變化。遞歸地檢測任意深度的屬性變化,,他最方便,,同時(shí)也最低效。
舉個(gè)例子,,模型發(fā)生變化時(shí),,上述三個(gè)方法的監(jiān)聽效果如下圖所示:
圖片來源: https://docs./guide/scope
$digest循環(huán)
曾經(jīng)有過Angular陰謀論,生成Angular是通過無限的循環(huán)來實(shí)時(shí)地刷新視圖,。事實(shí)上Angular要聰明地多,,因?yàn)橹挥胁僮靼l(fā)生時(shí)視圖才需要更新。
而這些有限的用戶操作都會產(chǎn)生DOM事件,,只需監(jiān)聽這些事件便可以做到實(shí)時(shí)地刷新視圖,。
Angular會監(jiān)聽網(wǎng)絡(luò)和DOM事件來自動更新視圖,下面以DOM事件為例來描述Angular如何進(jìn)行視圖的更新:
模板編譯階段
擁有ng-app 屬性的HTML元素會成為Angular模板,,在頁面載入時(shí)Angular會對它(以及它的子元素)進(jìn)行編譯(遞歸地匹配directive,、controller并綁定DOM事件)。
主要有兩個(gè)過程(以<input> 和ng-model 為例):
input Directive找到聲明了ng-model 屬性的<input> ,,綁定<input> 的keydown 事件,。
- 在
input Directive的上下文($scope )中添加對應(yīng)數(shù)據(jù)模型的$watch 函數(shù),,當(dāng)模型改變時(shí)操作并更新DOM。
input 是Angular內(nèi)置的一個(gè)Directive,,它會匹配<input> 元素,,并對它實(shí)現(xiàn)雙向的數(shù)據(jù)綁定。Angular對幾乎所有輸入型控件都編寫了Directive,。
運(yùn)行階段
- 用戶按鍵
x ,,瀏覽器觸發(fā)了keydown 事件。
input Directive中的事件處理函數(shù)被調(diào)用,,該處理函數(shù)中會執(zhí)行$apply("name = 'x'") ,。
- Angular 在當(dāng)前
$scope 中執(zhí)行($eval )表達(dá)式name = 'x' ,改變數(shù)據(jù)模型,。
- Angular 開始
$digest 循環(huán),。
$digest 會調(diào)用所有監(jiān)聽該模型的$watch listener,這些監(jiān)聽函數(shù)會更新各自負(fù)責(zé)的DOM,。
- 瀏覽器重新渲染DOM,。
這時(shí)一個(gè)示意圖:
強(qiáng)制刷新視圖
讓刷新視圖有很多方法:$scope.$digest , $scope.$apply , $timeout 。它們有各自的使用情景:
$digest
$digest 是$scope 下的方法,,調(diào)用它會導(dǎo)致當(dāng)前上下文的所有l(wèi)istener被執(zhí)行,。
因?yàn)閘istener可能會改變數(shù)據(jù),因此Angular會一遍一遍地調(diào)用這些listener直到數(shù)據(jù)不再改變,。但我們通常不會使用$digest ,,而是使用下面的$apply 。
$apply
$apply 也是$scope 下的方法,,它會導(dǎo)致$digest 被調(diào)用,。$apply 有一個(gè)可選參數(shù)(表達(dá)式字符串)。被調(diào)用時(shí),,傳入的表達(dá)式會被$eval 執(zhí)行,,接著$digest 會被調(diào)用。
通常我們應(yīng)使用$apply 來立即刷新視圖,。既然Angular會進(jìn)行視圖和模型的雙向綁定,,那么什么時(shí)候我們會需要顯式地調(diào)用$apply 呢?
Angular監(jiān)聽了DOM事件,、$http 事件來刷新視圖,。但當(dāng)我們使用實(shí)現(xiàn)自定義事件(尤其是使用第三方庫)時(shí),事件發(fā)生時(shí)Angular并不知道視圖需要更新,,此時(shí)便需要顯示地調(diào)用$apply ,。
例如,Bootstrap事件發(fā)生時(shí)、使用jQuery進(jìn)行AJAX時(shí),,websock et消息到達(dá)時(shí)。
$timeout
$timeout 也是ng Module下的Service,,它是window.setTimeout 的包裝,,用來延遲執(zhí)行一個(gè)函數(shù)。當(dāng)我們在Controller中更改了數(shù)據(jù)模型時(shí),,此時(shí)DOM還沒有得到更新($digest 循環(huán)還沒開始),。如果我們希望DOM刷新后執(zhí)行某些操作,就可以使用$timeout ,。例如:
module.controller('worldCtrl', ['$scope', '$timeout', function($scope, $timeout){
$scope.users = ['alice', 'bob'];
$timeout(function(){
$('.user').tooltip();
});
}]);
對應(yīng)的模板:
" data-hover-class="active"> <div ng-controller="worldCtrl">
<span class='user' ng-repeat='user in users'>{{user}}</span>
</div>
當(dāng)$scope.users = ['alice', 'bob']; 執(zhí)行后,,DOM中的<span> 還不存在,此時(shí)$('.user') 的值為空集,。盡管$timeout 沒有設(shè)置延遲時(shí)間(第二個(gè)參數(shù)),,但這樣的調(diào)用會使得回調(diào)函數(shù)在$digest 循環(huán)之后再執(zhí)行。
|