1 前言
前端技術的發展是如此之快,各種優秀技術、優秀框架的出現簡直讓人目不暇接,緊跟時代潮流,學習掌握新知識自然是不敢怠慢。
AngularJS是google在維護,其在國外已經十分火熱,可是國內的使用情況卻有不小的差距,參考文獻/網絡文章也很匱乏。這里便將我學習AngularJS寫成文檔,一方面作為自己學習路程上的記錄,另一方面也給有興趣的同學一些參考。
首先我自己也是一名學習者,會以學習者的角度來整理我的行文思路,這里可能只是些探索,有理解或是技術上的錯誤還請大家指出;其次我特別喜歡編寫小例子來把一件事情說明白,故在文中會盡可能多的用示例加代碼講解,我相信這會是一種比較好的方式;最后,我深知AngularJS的使用方式跟jquery的使用方式有很大不同,在大家都有jquery、ext經驗的條件下對於angular的學習會困難重重,不過我更相信在大家的堅持下,能夠快速的學好AngularJS,至少咱也能深入了解到AngularJS的基本思想,對咱們以后自己的插件開發、項目開發都會有很大的啟示。
2 AngularJS概述
2.1 AngularJS是什么?
AngularJs(后面就簡稱ng了)是一個用於設計動態web應用的結構框架。首先,它是一個框架,不是類庫,是像EXT一樣提供一整套方案用於設計web應用。它不僅僅是一個javascript框架,因為它的核心其實是對HTML標簽的增強。
何為HTML標簽增強?其實就是使你能夠用標簽完成一部分頁面邏輯,具體方式就是通過自定義標簽、自定義屬性等,這些HTML原生沒有的標簽/屬性在ng中有一個名字:指令(directive)。后面會詳細介紹。那么,什么又是動態web應用呢?與傳統web系統相區別,web應用能為用戶提供豐富的操作,能夠隨用戶操作不斷更新視圖而不進行url跳轉。ng官方也聲明它更適用於開發CRUD應用,即數據操作比較多的應用,而非是游戲或圖像處理類應用。
為了實現這些,ng引入了一些非常棒的特性,包括模板機制、數據綁定、模塊、指令、依賴注入、路由。通過數據與模板的綁定,能夠讓我們擺脫繁瑣的DOM操作,而將注意力集中在業務邏輯上。
另外一個疑問,ng是MVC框架嗎?還是MVVM框架?官網有提到ng的設計采用了MVC的基本思想,而又不完全是MVC,因為在書寫代碼時我們確實是在用ng-controller這個指令(起碼從名字上看,是MVC吧),但這個controller處理的業務基本上都是與view進行交互,這么看來又很接近MVVM。讓我們把目光移到官網那個非醒目的title上:“AngularJS — Superheroic JavaScript MVW Framework”。
2.2 AngularJS簡單介紹
AngularJS 重新定義了前端應用的開發方式。面對HTML和JavaScript之間的界線,它
非但不畏縮不前,反而正面出擊,提出了有效的解決方案。
很多前端應用的開發框架,比如Backbone、EmberJS等,都要求開發者繼承此框架特有的一些JavaScript對象。這種方式有其長處,但它不必要地污染了開發者自己代碼的對象空間,還要求開發者去了解內存里那些抽象對象。盡管如此我們還是接受了這種方式,因為網絡最初的設計無法提供 我們今天所需的交互性,於是我們需要框架,來幫我們填補JavaScript和HTML之間的鴻溝。而且有了它,你不用再“直接”操控DOM,只要給你的DOM注上metadata(即AngularJS里的directive們),然后讓AngularJS來幫你操縱DOM。同時,AngularJS不依賴(也不妨礙)任何其他的框架。你甚至可以基於其它的框架來開發AngularJS應用。
API地址:http://docs.angularjs.org/api/;
AngularJS在github上的中文粗譯版地址:https://github.com/basestyle/angularjs-cn。
2.3 什么時候該用AngularJS
AngularJS是一個 MV* 框架,最適於開發客戶端的單頁面應用。它不是個功能庫,而是用來開發動態網頁的框架。它專注於擴展HTML的功能,提供動態數據綁定(data binding),而且它能跟其它框架(如jQuery)合作融洽。
如果你要開發的是單頁應用,AngularJS就是你的上上之選。Gmail、Google Docs、Twitter和Facebook這樣的應用,都很能發揮AngularJS的長處。但是像游戲開發之類對DOM進行大量操縱、又或者單純需要 極高運行速度的應用,就不是AngularJS的用武之地了。
3 AugularJS特性
AngularJS是一個新出現的強大客戶端技術,提供給大家的一種開發強大應用的方式。這種方式利用並且擴展HTML,CSS和javascript,並且彌補了它們的一些非常明顯的不足。本應該使用HTML來實現而現在由它開發的動態一些內容。
AngularJS有五個最重要的功能和特性:
3.1 特性一:雙向的數據綁定
數據綁定可能是AngularJS最酷最實用的特性。它能夠幫助你避免書寫大量的初始代碼從而節約開發時間。一個典型的web應用可能包含了80%的代碼用來處理,查詢和監聽DOM。數據綁定是的代碼更少,你可以專注於你的應用。
我們想象一下Model是你的應用中的簡單事實。你的Model是你用來讀取或者更新的部分。數據綁定指令提供了你的Model投射到view的方法。這些投射可以無縫的,毫不影響的應用到web應用中。
傳統來說,當model變化了。 開發人員需要手動處理DOM元素並且將屬性反映到這些變化中。這個一個雙向的過程。一方面,model變化驅動了DOM中元素變化,另一方面,DOM元素的變化也會影響到Model。這個在用戶互動中更加復雜,因為開發人員需要處理和解析
這些互動,然后融合到一個model中,並且更新View。這是一個手動的復雜過程,當一個應用非常龐大的時候,將會是一件非常費勁的事情。
這里肯定有更好的解決方案!那就是AngularJS的雙向數據綁定,能夠同步DOM和Model等等。
這里有一個非常簡單的例子,用來演示一個input輸入框和<h1>元素的雙向綁定(例01):
<!doctype html>
<html ng-app="demoApp">
<head>
<script src="http://m.cnblogs.com/142260/js/angular.min.js" rel="nofollow"/>
</head>
<body>
<div>
<label>Name:</label>
<input type="text" ng-model="user.name" placeholder="請輸入名字">
<hr>
<h1>Hello, {{user.name}}!</h1>
</div>
</body>
</html>
說明:實際效果請大家看AngularJS/demo/index.html
3.2 特性二:模板
在AngularJS中,一個模板就是一個HTML文件。但是HTML的內容擴展了,包含了很多幫助你映射model到view的內容。
HTML模板將會被瀏覽器解析到DOM中。DOM然后成為AngularJS編譯器的輸入。AngularJS將會遍歷DOM模板來生成一些指導,即,directive(指令)。所有的指令都負責針對view來設置數據綁定。
我們要理解AuguarJS並不把模板當做String來操作。輸入AngularJS的是DOM而非string。數據綁定是DOM變化,不是字符串的連接或者innerHTML變化。使用DOM作為輸入,而不是字符串,是AngularJS區別於其它的框架的最大原因。使用DOM允許你擴展指令詞匯並且可以創建你自己的指令,甚至開發可重用的組件。
最大的好處是為設計師和開發者創建了一個緊密的工作流。設計師可以像往常一樣開發標簽,然后開發者拿過來添加上功能,通過數據綁定將會使得這個過程非常簡單。
這里有一個例子,我們使用ng-repeat指令來循環圖片數組並且加入img模板,如下:
function AlbumCtrl($scope) {
scope.images = [
{"image":"img/image_01.png", "description":"Image 01 description"},
{"image":"img/image_02.png", "description":"Image 02 description"},
{"image":"img/image_03.png", "description":"Image 03 description"},
{"image":"img/image_04.png", "description":"Image 04 description"},
{"image":"img/image_05.png", "description":"Image 05 description"}
];
}
<div ng-controller="AlbumCtrl">
<ul>
<li ng-repeat="image in images">
<img ng-src="http://m.cnblogs.com/142260/{{image.thumbnail}}" rel="nofollow"/>
</li>
</ul>
</div>
這里還有一件事值得提一句,AngularJS並不強制你學習一個新的語法或者從你的應用中提出你的模板。
3.3 特性三:MVC
針對客戶端應用開發AngularJS吸收了傳統的MVC基本原則。MVC或者Model-View-Controll設計模式針對不同的人可能意味不同的東西。AngularJS並不執行傳統意義上的MVC,更接近於MVVM(Moodel-View-ViewModel)。
Model
model是應用中的簡單數據。一般是簡單的javascript對象。這里沒有必要繼承框架的classes,使用proxy對象封裝或者使用特別的setter/getter方法來訪問。事實上我們處理vanilla javascript的方法就是一個非常好的特性,這種方法使得我們更少使用應用的原型。
ViewModel
viewmodel是一個用來提供特別數據和方法從而維護指定view的對象。
viewmodel是$scope的對象,只存在於AnguarJS的應用中。$scope只是一個簡單的js對象,這個對象使用簡單的API來偵測和廣播狀態變化。
Controller
controller負責設置初始狀態和參數化$scope方法用以控制行為。需要指出的controller並不保存狀態也不和遠程服務互動。
View
view是AngularJS解析后渲染和綁定后生成的HTML 。這個部分幫助你創建web應用的架構。$scope擁有一個針對數據的參考,controller定義行為,view處理布局和互動。
3.4 特性四:服務和依賴注入
AngularJS服務其作用就是對外提供某個特定的功能。
AngularJS擁有內建的依賴注入(DI)子系統,可以幫助開發人員更容易的開發,理解和測試應用。
DI允許你請求你的依賴,而不是自己找尋它們。比如,我們需要一個東西,DI負責找創建並且提供給我們。
為了而得到核心的AngularJS服務,只需要添加一個簡單服務作為參數,AngularJS會偵測並且提供給你:
function EditCtrl($scope, $location, $routeParams) {
// Something clever here...
}
你也可以定義自己的服務並且讓它們注入:
angular.module('MyServiceModule', []).
factory('notify', ['$window', function (win) {
return function (msg) {
win.alert(msg);
};
}]);
function myController(scope, notifyService) {
scope.callNotify = function (msg) {
notifyService(msg);
};
}
myController.$inject = ['$scope', 'notify'];
3.5 特性五:指令(Directives)
指令是我個人最喜歡的特性。你是不是也希望瀏覽器可以做點兒有意思的事情?那么AngularJS可以做到。
指令可以用來創建自定義的標簽。它們可以用來裝飾元素或者操作DOM屬性。可以作為標簽、屬性、注釋和類名使用。
這里是一個例子,它監聽一個事件並且針對的更新它的$scope ,如下:
myModule.directive('myComponent', function(mySharedService) {
return {
restrict: 'E',
controller: function($scope, $attrs, mySharedService) {
$scope.$on('handleBroadcast', function() {
$scope.message = 'Directive: ' + mySharedService.message;
});
},
replace: true,
template: '<input>'
};
});
然后,你可以使用這個自定義的directive來使用:
<my-component ng-model="message"></my-component>
使用一系列的組件來創建你自己的應用將會讓你更方便的添加,刪除和更新功能。
4 功能介紹
4.1數據綁定
AngularJS的雙向數據綁定,意味着你可以在Mode(JS)中改變數據,而這些變動立刻就會自動出現在View上,反之亦然。即:一方面可以做到model變化驅動了DOM中元素變化,另一方面也可以做到DOM元素的變化也會影響到Model。
在我們使用jQuery的時候,代碼中會大量充斥類似這樣的語句:var val = $(‘#id’).val(); $(‘#id’).html(str);等等,即頻繁的DOM操作(讀取和寫入),其實我們的最終目的並不是要操作DOM,而是要實現業務邏輯。ng的綁定將讓你擺脫DOM操作,只要模板與數據通過聲明進行了綁定,兩者將隨時保持同步,最新的數據會實時顯示在頁面中,頁面中用戶修改的數據也會實時被記錄在數據模型中。
從View到Controller再到View的數據交互(例01):
<html ng-app="demoApp">
……
<input type="text" ng-model="user.name" placeholder="請輸入名稱"/>
Hello, {{ user.name }}!
……
關鍵: ng-app 、 ng-model 和 { {user.name } }
首先: <html>元素的ng-app屬性。標識這個DOM里面的內容將啟用AngularJS應用。
其次:告訴AngularJS,對頁面上的“user.name” 這個Model進行雙向數據綁定。
第三:告訴AngularJS,在“{{ user.name}}”這個指令模版上顯示“user.name”這個Model的數據。
從Server到Controller再到View的數據交互(例02):
<html ng-app="demoApp">
……
<div ng-controller="demoController">
<input type="text" ng-model="user.name" disabled="disabled"/>
<a href="javascript:void(0);" target="_blank" rel="nofollow">獲取名字</a>
……
demoApp.controller("demoController", function($http, $scope){
$scope. getAjaxUser = function(){
// $http.get({url:"../xxx.action"}).success(function(data){
// $scope.user= data;
// });
$scope.user = {"name":"從JOSN中獲取的名稱","age":22};
};
});
改變$scope中的user,View也會自動更新。
4.2 scopes、module、controller
4.2.1 scopes
$scope是一個把view(一個DOM元素)連結到controller上的對象。在我們的MVC結構里,這個 $scope 將成為model,它提供一個綁定到DOM元素(以及其子元素)上的excecution context。
盡管聽起來有點復雜,但 $scope 實際上就是一個JavaScript對象,controller和view都可以訪問它,所以我們可以利用它在兩者間傳遞信息。在這個 $scope 對象里,我們既存儲數據,又存儲將要運行在view上的函數。
每一個Angular應用都會有一個 $rootScope。這個 $rootScope 是最頂級的scope,它對應着含有 ng-app 指令屬性的那個DOM元素。
app.run(function($rootScope) { $rootScope.name = "張三"; });
如果頁面上沒有明確設定 $scope ,Angular 就會把數據和函數都綁定到這里, 第一部分中的例子就是靠這一點成功運行的。
這樣,我們就可以在view的任何地方訪問這個name屬性,使用模版表達式{{}},像這樣:
{{ name }}
4.2.2 module
首先需要明確一下模板的概念。在我還不知道有模板這個東西的時候,曾經用js拼接出很長的HTML字符串,然后append到頁面中,這種方式想想真是又土又笨。后來又看到可以把HTML代碼包裹在一個<script>標簽中當作模板,然后按需要取來使用。
在ng中,模板十分簡單,它就是我們頁面上的HTML代碼,不需要附加任何額外的東西。在模板中可以使用各種指令來增強它的功能,這些指令可以讓你把模板和數據巧妙的綁定起來。
在<html>標簽上多了一個屬性ng-app=”MyApp”,它的作用就是用來指定ng的作用域是在<html>標簽以內部分。在js中,我們調用angular對象的module方法來聲明一個模塊,模塊的名字和ng-app的值對應。這樣聲明一下就可以讓ng運行起來了。
示例:
<html ng-app="demoApp">
var demoApp = angular.module('demoApp', []);
4.2.3 ng-controller
要明確創建一個$scope 對象,我們就要給DOM元素安上一個controller對象,使用的是ng-controller 指令屬性:
<div ng-controller="MyController"> {{ person.name }} </div>
ng-controller指令給所在的DOM元素創建了一個新的$scope 對象,並將這個$scope 對象包含進外層DOM元素的$scope 對象里。在上面的例子里,這個外層DOM元素的$scope 對象,就是$rootScope 對象。這個scope鏈是這樣的:
所有scope都遵循原型繼承(prototypal inheritance),這意味着它們都能訪問父scope們。對任何屬性和方法,如果AngularJS在當前scope上找不到,就會到父 scope上去找,如果在父scope上也沒找到,就會繼續向上回溯,一直到$rootScope 上。即如果controller是多層嵌套的,就會從最里面一直往外找,這個scope鏈是這樣的:
唯一的例外:有些指令屬性可以選擇性地創建一個獨立的scope,讓這個scope不繼承它的父scope們,這個會在指令詳解中說明。
4.3 ajax
$http 服務是AngularJS的核心服務之一,它幫助我們通過XMLHttpRequest對象或JSONP與遠程HTTP服務進行交流。
$http 服務是這樣一個函數:它接受一個設置對象,其中指定了如何創建HTTP請求;它將返回一個承諾(*參考JavaScript異步編程的promise模式),其中提供兩個方法: success方法和error方法。
demoApp.controller("demoController", function($http, $scope){
$scope. getAjaxUser = function(){
$http.get({url:"../xxx.action"}).success(function(data){
alert(data);
}).error(function(){
Alert(“出錯了!”);
});
};
});
AngularJS的AJAX與jquery等框架的AJAX基本一致,這里就不多說了。
4.4表達式
ng中的表達式與javascript表達式類似但是不可以划等號,它是ng自己定義的一套模式。表達式可以作為指令的值,如ng-modle=”people.name”、ng-click=”showMe()”,看起來是如此像字符串,故而也叫字符串表達式。也可以在標記中使用表達式,如{{1+2}},或者與過濾器一起使用{{1+2 | currency}}。在框架內部,字符串不會簡單的使用eval()來執行,而是有一個專門的$parse服務來處理。在ng表達式中不可以使用循環語句、判斷語句,事實上在模板中使用復雜的表達式也是一個不推薦的做法,這樣視圖與邏輯就混雜在一起了
我們在使用其他模板庫時,一般都會有模板的循環輸出、分支輸出、邏輯判斷等類似的控制。
要想理解指令屬性的運作,我們必須先理解表達式。在之前的例子里我們已經見過表達式,例如 {{ user.name }}。
請查看例03、例04、例05。
{{ 8 + 1 }} 9
{{ person }} {"name":"Ari Lerner"}
{{ 10 * 3.3 | currency }} $33.00
表達式粗略來看有點像 eval(javascript) 的結果。它們會經過Angular.js的處理,從而擁有以下重要而獨特的性質:
l 所有表達式都在scope這個context里被執行,因此可以使用所有本地 $scope 中的變量。
l 如果一個表達式的執行導致類型錯誤或引用錯誤,這些錯誤將不會被拋出。
l 表達式里不允許任何控制函數流程的功能(如if/else等條件語句)
l 表達式可接受一個或多個串聯起來的過濾器。
4.5過濾器
過濾器(filter)正如其名,作用就是接收一個輸入,通過某個規則進行處理,然后返回處理后的結果。主要用在數據的格式化上,例如獲取一個數組中的子集,對數組中的元素進行排序等。過濾器通常是伴隨標記來使用的,將你model中的數據格式化為需要的格式。表單的控制功能主要涉及到數據驗證以及表單控件的增強。ng內置了一些過濾器,它們是:
currency(貨幣)、date(日期)、filter(子串匹配)、json(格式化json對象)、limitTo(限制個數)、lowercase(小寫)、uppercase(大寫)、number(數字)、orderBy(排序)。
4.5.1過濾器使用方式
總共九種。除此之外還可以自定義過濾器,這個就強大了,可以滿足任何要求的數據處理。Filter還是很簡單的,需要明白的是內置的filter如何使用,以及自己如何定義一個filter。
filter的兩種使用方法:
1. 在模板中使用filter
我們可以直接在{{}}中使用filter,跟在表達式后面用 | 分割,語法如下:
{{ expression | filter }}
也可以多個filter連用,上一個filter的輸出將作為下一個filter的輸入:
{{ expression | filter1 | filter2 | ... }}
filter可以接收參數,參數用 : 進行分割,如下:
{{ expression | filter:argument1:argument2:... }}
除了對{{}}中的數據進行格式化,我們還可以在指令中使用filter,例如先對數組array進行過濾處理,然后再循環輸出:
<span ng-repeat="a in array | filter ">
2. 在controller和service中使用filter
我們的js代碼中也可以使用過濾器,方式就是我們熟悉的依賴注入,例如我要在controller中使用currency過濾器,只需將它注入到該controller中即可,代碼如下:
app.controller('testC',function($scope,currencyFilter){
$scope.num = currencyFilter(123534);
}
在模板中使用{{num}}就可以直接輸出$123,534.00了!在服務中使用filter也是同樣的道理。
如果你要在controller中使用多個filter,並不需要一個一個注入嗎,ng提供了一個$filter服務可以來調用所需的filter,你只需注入一個$filter就夠了,使用方法如下:
app.controller('testC',function($scope,$filter){
$scope.num = $filter('currency')(123534);
$scope.date = $filter('date')(new Date());
}
可以達到同樣的效果。好處是你可以方便使用不同的filter了。
4.5.2 ng的內置過濾器
ng內置了九種過濾器,使用方法都非常簡單,看文檔即懂。不過為了以后不去翻它的文檔,我在這里還是做一個詳細的記錄。
currency(貨幣)、date(日期)、filter(子串匹配)、json(格式化json對象)、limitTo(限制個數)、lowercase(小寫)、uppercase(大寫)、number(數字)、orderBy(排序)
1. currency (貨幣處理)
使用currency可以將數字格式化為貨幣,默認是美元符號,你可以自己傳入所需的符號,例如我傳入人民幣:
{{num | currency : '¥'}}
2. date (日期格式化)
原生的js對日期的格式化能力有限,ng提供的date過濾器基本可以滿足一般的格式化要求。用法如下:
{{date | date : 'yyyy-MM-dd hh:mm:ss EEEE'}}
參數用來指定所要的格式,y M d h m s E 分別表示 年 月 日 時 分 秒 星期,你可以自由組合它們。也可以使用不同的個數來限制格式化的位數。另外參數也可以使用特定的描述性字符串,例如“shortTime”將會把時間格式為12:05 pm這樣的。ng提供了八種描述性的字符串,個人覺得這些有點多余,我完全可以根據自己的意願組合出想要的格式,不願意去記這么多單詞~
3. filter(匹配子串)
這個名叫filter的filter。用來處理一個數組,然后可以過濾出含有某個子串的元素,作為一個子數組來返回。可以是字符串數組,也可以是對象數組。如果是對象數組,可以匹配屬性的值。它接收一個參數,用來定義子串的匹配規則。下面舉個例子說明一下參數的用法,我用現在特別火的幾個孩子定義了一個數組:
$scope.childrenArray = [
{name:'kimi',age:3},
{name:'cindy',age:4},
{name:'anglar',age:4},
{name:'shitou',age:6},
{name:'tiantian',age:5}
];
$scope.func = function(e){return e.age>4;}{{ childrenArray | filter : 'a' }} //匹配屬性值中含有a的
{{ childrenArray | filter : 4 }} //匹配屬性值中含有4的
{{ childrenArray | filter : {name : 'i'} }} //參數是對象,匹配name屬性中含有i的
{{childrenArray | filter : func }} //參數是函數,指定返回age>4的
4. json(格式化json對象)
json過濾器可以把一個js對象格式化為json字符串,沒有參數。這東西有什么用呢,我一般也不會在頁面上輸出一個json串啊,官網說它可以用來進行調試,嗯,是個不錯的選擇。或者,也可以用在js中使用,作用就和我們熟悉的JSON.stringify()一樣。用法超級簡單:
{{ jsonTest | json}}
5. limitTo(限制數組長度或字符串長度)
limitTo過濾器用來截取數組或字符串,接收一個參數用來指定截取的長度,如果參數是負值,則從數組尾部開始截取。個人覺得這個filter有點雞肋,首先只能從數組或字符串的開頭/尾部進行截取,其次,js原生的函數就可以代替它了,看看怎么用吧:
{{ childrenArray | limitTo : 2 }} //將會顯示數組中的前兩項
6. lowercase(小寫)
把數據轉化為全部小寫。太簡單了,不多解釋。同樣是很雞肋的一個filter,沒有參數,只能把整個字符串變為小寫,不能指定字母。怎么用我都懶得寫了。
7. uppercase(大寫)
同上。
8. number(格式化數字)
number過濾器可以為一個數字加上千位分割,像這樣,123,456,789。同時接收一個參數,可以指定float類型保留幾位小數:
{{ num | number : 2 }}
9. orderBy(排序)
orderBy過濾器可以將一個數組中的元素進行排序,接收一個參數來指定排序規則,參數可以是一個字符串,表示以該屬性名稱進行排序。可以是一個函數,定義排序屬性。還可以是一個數組,表示依次按數組中的屬性值進行排序(若按第一項比較的值相等,再按第二項比較),還是拿上面的孩子數組舉例:
<div>{{ childrenArray | orderBy : 'age' }}</div> //按age屬性值進行排序,若是-age,則倒序
<div>{{ childrenArray | orderBy : orderFunc }}</div> //按照函數的返回值進行排序
<div>{{ childrenArray | orderBy : ['age','name'] }}</div> //如果age相同,按照name進行排序 內置的過濾器介紹完了,寫的我都快睡着了。。。正如你所看到的,ng內置的過濾器也並不是萬能的,事實上好多都比較雞肋。更個性化的需求就需要我們來定義自己的過濾器了,下面來看看如何自定義過濾器。
4.5.3自定義過濾器及示例
filter的自定義方式也很簡單,使用module的filter方法,返回一個函數,該函數接收
輸入值,並返回處理后的結果。話不多說,我們來寫一個看看。比如我需要一個過濾器,它可以返回一個數組中下標為奇數的元素,代碼如下:
app.filter('odditems',function(){
return function(inputArray){
var array = [];
for(var i=0;i<inputArray.length;i++){
if(i%2!==0){
array.push(inputArray[i]);
}
}
return array;
}
});
格式就是這樣,你的處理邏輯就寫在內部的那個閉包函數中。你也可以讓自己的過濾器接收參數,參數就定義在return的那個函數中,作為第二個參數,或者更多個參數也可以。
自定義過濾器實例(例04):
/* View html */
First name:<input ng-model="user.firstName"/><br/>
Last name:<input ng-model="user.lastName"/> <br/>
First name:{{user.firstName}} Last name:{{user.lastName}} <br/>
Fullname:{{user | flFullname}}<br/>
Fullname:{{user | flFullname:"-"}}<br/>
Fullname:{{user | flFullname:"•" | uppercase }}
/* Controller js */
demoApp.filter("flFullname", function() {
return function(user, sep) {
sep = sep || " ";
user = user || {};
fullName = "";
if(user.firstName){fullName += user.firstName;}
if(user.lastName){fullName = fullName + sep + user.lastName;}
if(fullName && fullName.length>0){return fullName;
}else{return "";}
};
});
4.6指令(directive)
通過使用模板,我們可以把model和controller中的數據組裝起來呈現給瀏覽器,還可以通過數據綁定,實時更新視圖,讓我們的頁面變成動態的。
模板中可以使用的東西包括以下四種:
1.指令(directive):ng提供的或者自定義的標簽和屬性,用來增強HTML表現力;
2.標記(markup):即雙大括號{{}},可將數據單向綁定到HTML中;
3.過濾器(filter):用來格式化輸出數據;
4.表單控制:用來增強表單的驗證功能。
其中,指令無疑是使用量最大的,ng內置了很多指令用來控制模板,如ng-repeat,ng-class,也有很多指令來幫你完成業務邏輯,如ng-controller,ng-model。
指令的幾種使用方式如下:
l 作為標簽:<my-dir></my-dir>
l 作為屬性:<span my-dir="exp"></span>
l 作為注釋:<!-- directive: my-dir exp -->
l 作為類名:<span class="my-dir: exp;"></span>
其實常用的就是作為標簽和屬性。
4.6.1樣式相關的指令
既然模板就是普通的HTML,那我首要關心的就是樣式的控制,元素的定位、字體、背景色等等如何可以靈活控制。下面來看看常用的樣式控制指令。
1. ng-class
ng-class用來給元素綁定類名,其表達式的返回值可以是以下三種:
l 類名字符串,可以用空格分割多個類名,如’redtext boldtext’;
l 類名數組,數組中的每一項都會層疊起來生效;
l 一個名值對應的map,其鍵值為類名,值為boolean類型,當值為true時,該類會被加在元素上。
下面來看一個使用map的例子:
ng-class測試
紅色 加粗 刪除線
map:{redtext:{{red}}, boldtext:{{bold}}, striketext:{{strike}}}
如果你想拼接一個類名出來,可以使用插值表達式,如:
<div class=”{{style}}text”>字體樣式測試</div>
然后在controller中指定style的值:
$scope.style = ‘red’;
注意我用了class而不是ng-class,這是不可以對換的,官方的文檔也未做說明,姑且認為這是ng的語法規則吧。
與ng-class相近的,ng還提供了ng-class-odd、ng-class-even兩個指令,用來配合ng-repeat分別在奇數列和偶數列使用對應的類。這個用來在表格中實現隔行換色再方便不過了。
2. ng-style
ng-style用來綁定元素的css樣式,其表達式的返回值為一個js對象,鍵為css樣式名,值為該樣式對應的合法取值。用法比較簡單:
<div ng-style="{color:'red'}">ng-style測試</div>
<div ng-style="style">ng-style測試</div>
$scope.style = {color:'red'};
3. ng-show,ng-hide
對於比較常用的元素顯隱控制,ng也做了封裝,ng-show和ng-hide的值為boolean類型的表達式,當值為true時,對應的show或hide生效。框架會用display:block和display:none來控制元素的顯隱。
4.6.2表單控件功能相關指令
對於常用的表單控件功能,ng也做了封裝,方便靈活控制。
ng-checked控制radio和checkbox的選中狀態
ng-selected控制下拉框的選中狀態
ng-disabled控制失效狀態
ng-multiple控制多選
ng-readonly控制只讀狀態
以上指令的取值均為boolean類型,當值為true時相關狀態生效,道理比較簡單就不多做解釋。注意: 上面的這些只是單向綁定,即只是從數據到模板,不能反作用於數據。要雙向綁定,還是要使用 ng-model 。
4.6.3事件綁定相關指令
事件綁定是javascrpt中比較重要的一部分內容,ng對此也做了詳細的封裝,正如我們之前使用過的ng-click一樣,事件的指令如下:
ng-click
ng-change
ng-dblclick
ng-mousedown
ng-mouseenter
ng-mouseleave
ng-mousemove
ng-mouseover
ng-mouseup
ng-submit
事件綁定指令的取值為函數,並且需要加上括號,例如:
<select ng-change=”change($event)”></select>
然后在controller中定義如下:
$scope.change = function($event){
alert($event.target);
//……………………
}
在模板中可以用變量$event將事件對象傳遞到controller中。
對於ng的這種設計,一些人有所質疑,視圖與事件綁定混在一起到底好不好?我們不是要講究視圖與邏輯分離嗎?如此一來,把事件的綁定又變回了內聯的,豈不是歷史的倒退。我也一樣對此表示不解,因為不寫onclick已經很多年。。。但既然已經存在了,我們不妨往合理的方向上想一想,或許ng的設計者壓根就不想讓模板成為單純的視圖層,本來就是想增強HTML,讓它有一點業務能力。這么想的話似乎也能想通,好吧,先欺騙一下自己吧~
4.6.4特殊的ng-src和ng-href
在說明這兩個指令的特殊之前,需要先了解一下ng的啟動及執行過程,如下圖:
1) 瀏覽器加載靜態HTML文件並解析為DOM;
2) 瀏覽器加載angular.js文件;
3) angular監聽DOMContentLoaded 事件,監聽到時開始啟動;
4) angular尋找ng-app指令,確定作用范圍;
5) 找到app中定義的Module使用$injector服務進行依賴注入;
6) 根據$injector服務創建$compile服務用於編譯;
7) $compile服務編譯DOM中的指令、過濾器等;
8) 使用ng-init指令,將作用域中的變量進行替換;
9) 最后生成了我們在最終視圖。
可以看到,ng框架是在DOMcontent加載完畢后才開始發揮作用。假如我們模板中有一張圖片如下:
<img src="http://m.cnblogs.com/142260/”{{imgUrl}}” />
那么在頁面開始加載到ng編譯完成之前,頁面上會一直顯示一張錯誤的圖片,因為路徑{{imgUrl}}還未被替換。
為了避免這種情況,我們使用ng-src指令,這樣在路徑被正確得到之前就不會顯示找不到圖片。同理,<a>標簽的href屬性也需要換成ng-href,這樣頁面上就不會先出現一個地址錯誤的鏈接。
順着這個思路再多想一點,我們在模板中使用{{}}顯示數據時,在ng編譯完成之前頁面上豈不是會顯示出大括號及里面的表達式?確實是這樣。為了避免這個,ng中有一個與{{}}等同的指令:ng-bind,同樣用於單向綁定,在頁面剛加載的時候就不會顯示出對用戶無用的數據了。盡管這樣你可能不但沒舒心反而更糾結了,{{}}那么好用易理解,還不能用了不成?好消息是我們依然可以使用。因為我編寫的是單頁面應用,頁面只會在加載index.html的時
候出這個問題,只需在index.html中的模板中換成ng-bind就行。其他的模板是我們動態加載的,就可以放心使用{{}}了。
4.6.5 自定義指令示例
下面我們來解析下指令的例子(例07)。
1.首先,我們定義一個名為userInfo的指令:
demoApp.directive('userInfo',function(){
return {
restrict : 'E',
templateUrl : 'userInfoTemplate.html',
replace : true,
transclude : true,
scope : {
mytitle : '=etitle'
},
link : function(scope,element,attrs){
scope.showText = false;
scope.toggleText = function(){
scope.showText = ! scope.showText;
}
}
};
})
Restrict為'E':用作標簽;replace為true:用模板替換當前標簽;transclude為true:將當前元素的內容轉移到模板中;scope 為 {mytitle : '=etitle'}:定義一個名為mytitle的MODEL,其值指向當前元素的etitle屬性;templateUrl為'userInfoTemplate.html':模板內容為ng-template定義ID為userInfoTemplate.html的內容;link:指定所包含的行為。其具體的說明及其他參數,請參考:6.2指令詳解。
2. userInfoTemplate.html模板為:
<script type="text/ng-template" id="userInfoTemplate.html">
<div class="mybox">
<div class="mytitle" style="cursor: pointer;" ng-click="toggleText()">
{ {mytitle} }
</div>
<div ng-transclude ng-show="showText">
</div>
</div>
</script>
將當前元素的內容添加到有ng-transclude屬性的這個DIV下,默認是隱藏的。
3.Controller信息:
demoApp.controller("test7Controller", function($scope){
$scope.title = '個人簡介';
$scope.text = '大家好,我正在研究AngularJs,歡迎大家與我交流。';
$scope.updateInfo = function (){
$scope.title = '個人信息';
$scope.text = '大家好,今天天氣真好!';
}
});
4.指令使用方式(View信息)為:
<user-info etitle="title">{ {text} }</user-info>
Etitle指向Controller中的$scope.title。注意命名方式:指令名為userInfo,對應的標簽為user-info。
4.7服務(service)
4.7.1服務介紹
服務這個概念其實並不陌生,在其他語言中如java便有這樣的概念,其作用就是對外提供某個特定的功能,如消息服務,文件壓縮服務等,是一個獨立的模塊。ng的服務是這樣定義的:
Angular services are singletons objects or functions that carry out specific tasks common to web apps.
它是一個單例對象或函數,對外提供特定的功能。
首先是一個單例,即無論這個服務被注入到任何地方,對象始終只有一個實例。
其次這與我們自己定義一個function然后在其他地方調用不同,因為服務被定義在一個模塊中,所以其使用范圍是可以被我們管理的。ng的避免全局變量污染意識非常強。
ng提供了很多內置的服務,可以到API中查看http://docs.angularjs.org/api/。知道了概念,我們來拉一個service出來溜溜,看看到底是個什么用法。
我們在controller中直接聲明$location服務,這依靠ng的依賴注入機制。$location提供地址欄相關的服務,我們在此只是簡單的獲取當前的地址。
服務的使用是如此簡單,我們可以把服務注入到controller、指令或者是其他服務中。
4.7.2自定義服務
如同指令一樣,系統內置的服務以$開頭,我們也可以自己定義一個服務。定義服務的方式有如下幾種:
l 使用系統內置的$provide服務;
l 使用Module的factory方法;
l 使用Module的service方法。
下面通過一個小例子來分別試驗一下。我們定義一個名為remoteData服務,它可以從遠程獲取數據,這也是我們在程序中經常使用的功能。不過我這里沒有遠程服務器,就寫死一點數據模擬一下。
//使用$provide來定義
var app = angular.module('MyApp', [], function($provide) {
$provide.factory('remoteData', function() {
var data = {name:'n',value:'v'};
return data;
});
});
//使用factory方法
app.factory('remoteData',function(){
var data = {name:'n',value:'v'};
return data;
});
//使用service方法
app.service('remoteData',function(){
this.name = 'n';
this.value = 'v';
});
Module的factory和$provide的factory方法是一模一樣的,從官網文檔看它們其實就是一回事。至於Module內部是如何調用的,我此處並不打算深究,我只要知道怎么用就好了。
再看Module的service方法,它沒有return任何東西,是因為service方法本身返回一個構造器,系統會自動使用new關鍵字來創建出一個對象。所以我們看到在構造器函數內可以使用this,這樣調用該服務的地方便可以直接通過remoteData.name來訪問數據了。
4.7.3管理服務的依賴關系
服務與服務中間可以有依賴關系,例如我們這里定義一個名為validate的服務,它的作用是驗證數據是否合法,它需要依賴我們從遠程獲取數據的服務remoteData。代碼如下:
在factory的參數中,我們可以直接傳入服務remoteData,ng的依賴注入機制便幫我們做好了其他工作。不過一定要保證這個參數的名稱與服務名稱一致,ng是根據名稱來識別的。若參數的名次與服務名稱不一致,你就必須顯示的聲明一下,方式如下:
app.factory('validate',['remoteData',function(remoteDataService){
return function(){
if(remoteDataService.name=='n'){
alert('驗證通過');
}
};
}]);
我們在controller中注入服務也是同樣的道理,使用的名稱需要與服務名稱一致才可以正確注入。否則,你必須使用$inject來手動指定注入的服務。比如:
function testC(scope,rd){
scope.getData = function(){
alert('name:'+rd.name+' value:'+rd.value);
}
}
testC.$inject = ['$scope','remoteData'];
在controller中注入服務,也可以在定義controller時使用數組作為第二個參數,在此處
把服務注入進去,這樣在函數體中使用不一致的服務名稱也是可以的,不過要確保注入的順序是一致的,如:
app.controller('testC',['$scope','remoteData',function($scope,rd){
$scope.getData = function(){
alert('name:'+rd.name+' value:'+rd.value);
}
}]);
4.7.4 自定義服務示例
接下來讓我們看下例子(例08 自定義服務)代碼,自定義userService服務:
demoApp.factory('userService', ['$http', function($http) {
var doGetUser = function(userId, path) {
//return $http({
//method: 'JSONP',
//url: path
//});
/*手動指定數據*/
var data = {userId:"woshishui",userName:"我是誰",userInfo:"我是誰!我是誰!"};;
if(userId=='zhangsan'){
data = {userId:"zhangsan",userName:"張三",userInfo:"我是張三,我為自己"};
}else if(userId=='lisi'){
data = {userId:"lisi",userName:"李四",userInfo:"我是李四,我為卿狂!"};
}
return data;
}
return {
/*userService對外暴露的函數,可有多個*/
getUser: function(userId) {
return doGetUser(userId, '../xxx/xxx.action');
}
};
}]);
我們創建了一個只有一個方法的userService,getUser為這個服務從后台獲取用戶信息的函數,並且對外暴露。當然,由於這是一個靜態的例子,無法訪問后台,那么我們便制定其返回的數據。
然后我們把這個服務添加到我們的controller中。我們建立一個controller並加載(或者注入)userService作為運行時依賴,我們把service的名字作為參數傳遞給controller 函數:
demoApp.controller("test8Controller", function($scope,userService){
/*文章信息*/
$scope.articles = [{
title : "愛飛像風",
userId : "zhangsan",
userName : "張三"
},{
title : "無法停止的雨",
userId : "lisi",
userName : "李四"
}];
$scope.showUserInfo = false;//顯示作者詳細信息開關
$scope.currentUser = {}; //當前選中的作者
$scope.getUserInfo = function(userId){
$scope.currentUser = userService.getUser(userId);
//調用 userService的getUser函數
$scope.showUserInfo = true;
setTimeout(function(){//定時器:隱藏作者詳細信息
$scope.showUserInfo = false;
},3000);
}
});
我們的userService注入到我們的test8Controller后,我們就可以像使用其他服務(我們前面提到的$http服務)一樣的使用userService了。
相關的HTML代碼如下:
/* View HTML*/
<tr ng-repeat="article_ in articles">
<td>
{{article_.title}}
</td>
<td>
<a href="javascript:void(0);" target="_blank" rel="nofollow">
</td>
</tr>
......
<div ng-show="showUserInfo">
用戶ID:{{currentUser.userId}}<br/>
用戶名:{{currentUser.userName}}<br/>
用戶簡介:{{currentUser.userInfo}}<br/>
</div>
4.8依賴注入DI
通過依賴注入,ng想要推崇一種聲明式的開發方式,即當我們需要使用某一模塊或服務時,不需要關心此模塊內部如何實現,只需聲明一下就可以使用了。在多處使用只需進行多次聲明,大大提高可復用性。
比如我們的controller,在定義的時候用到一個$scope參數。
app.controller('testC',function($scope){});
如果我們在此處還需操作其他的東西,比如與瀏覽器地址欄進行交互。我們只需再多添
一個參數$location進去:
app.controller('testC',function($scope,$location){});
這樣便可以通過$location來與地址欄進行交互了,我們僅僅是聲明了一下,所需的其他代碼,框架已經幫我們注入了。我們很明顯的感覺到了這個函數已經不是常規意義上的javascript函數了,在常規的函數中,把形參換一個名字照樣可以運行,但在此處若是把$scope換成別的名字,程序便不能運行了。因為這是已經定義好的服務名稱。
這便是依賴注入機制。順理成章的推斷,我們可以自己定義模塊和服務,然后在需要的地方進行聲明,由框架來替我們注入。
來看下我們如何定義一個服務:
app.factory('tpls',function(){
return ['tpl1','tpl2','tpl3','tpl4'];
});
看上去相當簡單,是因為我在這里僅僅是直接返回一個數組。在實際應用中,這里應該是需要向服務器發起一個請求,來獲取到這些模板們。服務的定義方式有好幾種,包括使用provider方法、使用factory方法,使用service方法。它們之間的區別暫且不關心。我們現在只要能創建一個服務出來就可以了。我使用了factory方法。一個需要注意的地方是,框架提供的服務名字都是由$開頭的,所以我們自己定義的最好不要用$開頭,防止發生命名沖突。
定義好一個服務后,我們就可以在控制器中聲明使用了,如下:
app.controller('testC',function($scope,tpls){
$scope.question = questionModel;
$scope.nowTime = new Date().valueOf();
$scope.templates = tpls; //賦值到$scope中
$scope.addOption = function(){
var o = {content:''};
$scope.question.options.push(o);
};
$scope.delOption = function(index){
$scope.question.options.splice(index,1);
};
});
此時,若在模板中書寫如下代碼,我們便可以獲取到服務tpls所提供的數據了:
模板:
<a href="javascript:void(0);" target="_blank" rel="nofollow">
4.9路由(route)
在談路由機制前有必要先提一下現在比較流行的單頁面應用,就是所謂的single page APP。為了實現無刷新的視圖切換,我們通常會用ajax請求從后台取數據,然后套上HTML模板渲染在頁面上,然而ajax的一個致命缺點就是導致瀏覽器后退按鈕失效,盡管我們可以在頁面上放一個大大的返回按鈕,讓用戶點擊返回來導航,但總是無法避免用戶習慣性的點后退。解決此問題的一個方法是使用hash,監聽hashchange事件來進行視圖切換,另一個方法是用HTML5的history API,通過pushState()記錄操作歷史,監聽popstate事件來進行視圖切換,也有人把這叫pjax技術。基本流程如下:
如此一來,便形成了通過地址欄進行導航的深度鏈接(deeplinking ),也就是我們所需要的路由機制。通過路由機制,一個單頁應用的各個視圖就可以很好的組織起來了。
4.9.1 ngRoute內容
ng的路由機制是靠ngRoute提供的,通過hash和history兩種方式實現了路由,可以檢測瀏覽器是否支持history來靈活調用相應的方式。ng的路由(ngRoute)是一個單獨的模塊,包含以下內容:
l 服務$routeProvider用來定義一個路由表,即地址欄與視圖模板的映射
l 服務$routeParams保存了地址欄中的參數,例如{id : 1, name : 'tom'}
l 服務$route完成路由匹配,並且提供路由相關的屬性訪問及事件,如訪問當前路由對應的controller
l 指令ngView用來在主視圖中指定加載子視圖的區域
以上內容再加上$location服務,我們就可以實現一個單頁面應用了。下面來看一下具體如何使用這些內容。
4.9.2 ng的路由機制
第一步:引入文件和依賴
ngRoute模塊包含在一個單獨的文件中,所以第一步需要在頁面上引入這個文件,如下:
<script src="http://code.angularjs.org/1.2.8/angular.min.js" rel="nofollow"/>
<script src="http://code.angularjs.org/1.2.8/angular-route.min.js" rel="nofollow"/>
光引入還不夠,我們還需在模塊聲明中注入對ngRoute的依賴,如下:
var app = angular.module('MyApp', ['ngRoute']);
完成了這些,我們就可以在模板或是controller中使用上面的服務和指令了。下面我們需要定義一個路由表。
第二步:定義路由表
$routeProvider提供了定義路由表的服務,它有兩個核心方法,when(path,route)和otherwise(params),先看一下核心中的核心when(path,route)方法。
when(path,route)方法接收兩個參數,path是一個string類型,表示該條路由規則所匹配的路徑,它將與地址欄的內容($location.path)值進行匹配。如果需要匹配參數,可以在path中使用冒號加名稱的方式,如:path為/show/:name,如果地址欄是/show/tom,那么參數name和所對應的值tom便會被保存在$routeParams中,像這樣:{name : tom}。我們也可以用*進行模糊匹配,如:/show*/:name將匹配/showInfo/tom。
route參數是一個object,用來指定當path匹配后所需的一系列配置項,包括以下內容:
l controller //function或string類型。在當前模板上執行的controller函數,生成新的scope;
l controllerAs //string類型,為controller指定別名;
l template //string或function類型,視圖z所用的模板,這部分內容將被ngView引用;
l templateUrl //string或function類型,當視圖模板為單獨的html文件或是使用了<script type="text/ng-template">定義模板時使用;
l resolve //指定當前controller所依賴的其他模塊;
l redirectTo //重定向的地址。
最簡單情況,我們定義一個html文件為模板,並初始化一個指定的controller:
function emailRouteConfig($routeProvider){
$routeProvider.when('/show', {
controller: ShowController,
templateUrl: 'show.html'
}).
when('/put/:name',{
controller: PutController,
templateUrl: 'put.html'
});
};
otherwise(params)方法對應路徑匹配不到時的情況,這時候我們可以配置一個redirectTo參數,讓它重定向到404頁面或者是首頁。
第三步:在主視圖模板中指定加載子視圖的位置
我們的單頁面程序都是局部刷新的,那這個“局部”是哪里呢,這就輪到ngView出馬了,只需在模板中簡單的使用此指令,在哪里用,哪里就是“局部”。例如:
<div ng-view></div> 或:<ng-view></ng-view>
我們的子視圖將會在此處被引入進來。完成這三步后,你的程序的路由就配置好了。
4.9.3 路由示例
下面我們將用一個例子(例09)來說明路由的使用方式及步驟:
1.為demoApp添加一個路由,代碼如下:
demoApp.config(['$routeProvider',function($routeProvider) {
$routeProvider.when('/list', {
templateUrl: 'route/list.html',
controller: 'routeListController'
}).when('/list/:id', {
templateUrl: 'route/detail.html',
controller: 'routeDetailController'
}).otherwise({
redirectTo: '/list'
});
}]);
/list 對應為:route/list.html頁面,顯示用戶列表;/list/:id對應於route/detail.html頁面,顯示用戶詳細信息。
2.為list.html和detail.html分別聲明Controller:routeListController和routeDetailController。
demoApp.controller('routeListController',function($scope) {
$scope.users = [{userId:"zhangsan",userName:"張三",userInfo:"我是張三,我為自己帶鹽!"},
{userId:"lisi",userName:"李四",userInfo:"我是李四,我為卿狂!"},
{userId:"woshishui",userName:"我是誰",userInfo:"我是誰!我是誰!我是誰!"}];
});
demoApp.controller('routeDetailController',function($scope, $routeParams, userService) {
$scope.userDetail = userService.getUser($routeParams.id);
});
routeDetailController中如上面提到的一樣,注入了userService服務,在這里直接拿來用。
3.創建list.html和detail.html頁面,代碼如下:
<hr/>
<h3>Route : List.html(用戶列表頁面)</h3>
<ul>
<li ng-repeat="user in users">
<a href="http://m.cnblogs.com/142260/3817063.html?full=1#/list/{{ user.userId }}" target="_blank" rel="nofollow">
</li>
</ul>
<hr/>
<h3>Route : detail.html(用戶詳細信息頁面)</h3>
<h3>用戶名:<span style="color: red;">{{userDetail.userName}}</span></h3>
<div>
<span>用戶ID:{{userDetail.userId}}</span><span>用戶名:{{userDetail.userName}}</span>
</div>
<div>
用戶簡介:<span>{{userDetail.userInfo}}</span>
</div>
<div>
<a href="http://m.cnblogs.com/142260/3817063.html?full=1#/list" target="_blank" rel="nofollow">返回</a>
</div>
4. 路由局部刷新位置:
<h1>AngularJS路由(Route) 示例</h1>
<div ng-view></div>
4.10 NG動畫效果
4.10.1 NG動畫效果簡介
NG動畫效果,現在可以通過CSS3或者是JS來實現,如果是通過JS來實現的話,需要其他JS庫(比如JQuery)來支持,實際上底層實現還是靠其他JS庫,只是NG將其封裝了,
使其更易使用。
NG動畫效果包含以下幾種:
- enter:元素添加到DOM中時執行動畫;
- leave:元素從DOM刪除時執行動畫;
- move:移動元素時執行動畫;
- beforeAddClass:在給元素添加CLASS之前執行動畫;
- addClass:在給元素添加CLASS時執行動畫;
- beforeRemoveClass:在給元素刪除CLASS之前執行動畫;
- removeClass:在給元素刪除CLASS時執行動畫。
其相關參數為:
var ngModule = angular.module('YourApp', ['ngAnimate']);
demoApp.animation('.my-crazy-animation', function() {
return {
enter: function(element, done) {
//run the animation here and call done when the animation is complete
return function(cancelled) {
//this (optional) function will be called when the animation
//completes or when the animation is cancelled (the cancelled
//flag will be set to true if cancelled).
};
},
leave: function(element, done) { },
move: function(element, done) { },
//animation that can be triggered before the class is added
beforeAddClass: function(element, className, done) { },
//animation that can be triggered after the class is added
addClass: function(element, className, done) { },
//animation that can be triggered before the class is removed
beforeRemoveClass: function(element, className, done) { },
//animation that can be triggered after the class is removed
removeClass: function(element, className, done) { }
};
});
4.10.2 動畫效果示例
下面我們來看下DEMO中的例子(例10)。
1.首先,我們在demoApp下定義一個動畫效果,匹配CLASS:” .border-animation”
/*定義動畫*/
demoApp.animation('.border-animation', function(){
return{
beforeAddClass : function (element, className, done) {
$(element).stop().animate({
'border-width':1
},2000, function() {
done();
});
},
removeClass : function (element ,className ,done ) {
$(element).stop().animate({
'border-width':50
},3000, function() {
done();
});
}
};
});
動畫效果的含義就是:在匹配CLASS為border-animation的元素添加一個CLASS之前使其邊框的寬度在2秒內變為1PX;並在其移除一個CLASS時使其邊框的寬度在3秒內變為50PX。
2. 視圖中的代碼如下(主要,其他相關樣式請查看例子代碼):
<div class="border-animation" ng-show="testShow"></div>
<a href="javascript:void(0);" target="_blank" rel="nofollow">
ng-show為false時會為其加上“ng-hide“的CLASS; ng-show為true時會為其移除“ng-hide“的CLASS,從而觸發動畫效果。
3.其他代碼:
demoApp.controller("test10Controller", function($scope, $animate){
$scope.testShow = true;
});
5 功能演示
略(詳情請看AngularJS/demo WEB演示)
6 AngularJS進階
6.1數據綁定原理研究
Angular用戶都想知道數據綁定是怎么實現的。你可能會看到各種各樣的詞匯:$watch、$apply、$digest、dirty-checking...它們是什么?它們是如何工作的呢?這里我想回答這些問題,其實它們在官方的文檔里都已經回答了,但是我還是想把它們結合在一起來講,但是我只是用一種簡單的方法來講解,如果要想了解技術細節,查看源代碼。
6.1.1 AngularJS擴展事件循環
我們的瀏覽器一直在等待事件,比如用戶交互。假如你點擊一個按鈕或者在輸入框里輸入東西,事件的回調函數就會在javascript解釋器里執行,然后你就可以做任何DOM操作,等回調函數執行完畢時,瀏覽器就會相應地對DOM做出變化。(記住,這是個重要的概念),為了解釋什么是context以及它如何工作,我們還需要解釋更多的概念。
6.1.2 $watch 隊列
每次你綁定一些東西到你的DOM上時你就會往$watch隊列里插入一條$watch。想象一下$watch就是那個可以檢測它監視的model里時候有變化的東西。例如你有如下的代碼:
/*View index.html */
User: <input type="text" ng-model="user" />
Password: <input type="password" ng-model="pass" />
在這里我們有個$scope.user,他被綁定在了第一個輸入框上,還有個$scope.pass,它被綁定在了第二個輸入框上,然后我們在$watch list里面加入兩個$watch。
再看下面的例子:
/*Controller controllers.js */
app.controller('MainCtrl', function($scope) {
$scope.foo = "Foo";
$scope.world = "World";
});
/*View index.html */
Hello, {{ World }}
這里,即便我們在$scope上添加了兩個東西,但是只有一個綁定在了DOM上,因此在這里只生成了一個$watch。
再看下面的例子:
/*Controller controllers.js */
app.controller('MainCtrl', function($scope) {
$scope.people = [...];
});
/*View index.html */
<ul>
<li ng-repeat="person in people">
{{person.name}} - {{person.age}}
</li>
</ul>
這里又生成了多少個$watch呢?每個person有兩個(一個name,一個age),然后ng-repeat又有一個,因此10個person一共是(2 * 10) +1,也就是說有21個$watch。
因此,每一個綁定到了DOM上的數據都會生成一個$watch。
那這寫$watch是什么時候生成的呢?
當我們的模版加載完畢時,也就是在linking階段(Angular分為compile階段和linking階段),Angular解釋器會尋找每個directive,然后生成每個需要的$watch。
6.1.3 $digest循環
還記得我前面提到的擴展的事件循環嗎?當瀏覽器接收到可以被angular context處理的事件時,$digest循環就會觸發。這個循環是由兩個更小的循環組合起來的。一個處理evalAsync隊列,另一個處理$watch隊列。 這個是處理什么的呢?$digest將會遍歷我們的$watch,然后詢問:
•嘿,$watch,你的值是什么?
◦是9。
•好的,它改變過嗎?
◦沒有,先生。
•(這個變量沒變過,那下一個)
•你呢,你的值是多少?
◦報告,是Foo。
•剛才改變過沒?
◦改變過,剛才是Bar。
•(很好,我們有DOM需要更新了)
•繼續詢問直到$watch隊列都檢查過。
這就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問了:有沒有$watch更新過?如果有至少一個更新過,這個循環就會再次觸發,直到所有的$watch都沒有變化。這樣就能夠保證每個model都已經不會再變化。記住如果循環超過10次的話,它將會拋出一個異常,防止無限循環。當$digest循環結束時,DOM相應地變化。
例如:
/*Controller controllers.js */
app.controller('MainCtrl', function() {
$scope.name = "Foo";
$scope.changeFoo = function() {
$scope.name = "Bar";
}
});
/*View index.html */
{{ name }}
<button ng-click="changeFoo()">Change the name</button>
這里我們有一個$watch因為ng-click不生成$watch(函數是不會變的)。
我們可以看出ng的處理流程:
•我們按下按鈕;
•瀏覽器接收到一個事件,進入angular context;
•$digest循環開始執行,查詢每個$watch是否變化;
•由於監視$scope.name的$watch報告了變化,它會強制再執行一次$digest循環;
•新的$digest循環沒有檢測到變化;
•瀏覽器拿回控制權,更新與$scope.name新值相應部分的DOM。
這里很重要的是每一個進入angular context的事件都會執行一個$digest循環,也就是說每次我們輸入一個字母循環都會檢查整個頁面的所有$watch。
6.1.4如何進入angular context
誰決定什么事件進入angular context,而哪些又不進入呢?通過$apply!
如果當事件觸發時,你調用$apply,它會進入angular context,如果沒有調用就不會進入。現在你可能會問:剛才的例子里我也沒有調用$apply啊,為什么?Angular已經做了!因此你點擊帶有ng-click的元素時,時間就會被封裝到一個$apply調用。如果你有一個ng-model="foo"的輸入框,然后你敲一個f,事件就會這樣調用$apply("foo = 'f';")。
Angular什么時候不會自動為我們$apply呢?
這是Angular新手共同的痛處。為什么我的jQuery不會更新我綁定的東西呢?因為jQuery沒有調用$apply,事件沒有進入angular context,$digest循環永遠沒有執行。
我們來看一個有趣的例子:
假設我們有下面這個directive和controller。
/*Controller app.js */
app.directive('clickable', function() {
return {
restrict: "E",
scope: {
foo: '=',
bar: '='
},
template: '<ul style="<li>{{foo}}</li><li>{{bar}}</li></ul>',
link: function(scope, element, attrs) {
element.bind('click', function() {
scope.foo++;
scope.bar++;
});
}
}
});
app.controller('MainCtrl', function($scope) {
$scope.foo = 0;
$scope.bar = 0;
});
它將foo和bar從controller里綁定到一個list里面,每次點擊這個元素的時候,foo和bar都會自增1。那我們點擊元素的時候會發生什么呢?我們能看到更新嗎?答案是否定的。因為點擊事件是一個沒有封裝到$apply里面的常見的事件,這意味着我們會失去我們的計數嗎?不會。
真正的結果是:$scope確實改變了,但是沒有強制$digest循環,監視foo 和bar的$watch沒有執行。也就是說如果我們自己執行一次$apply那么這些$watch就會看見這些變化,然后根據需要更新DOM。
執行$apply:
element.bind('click', function() {
scope.foo++;
scope.bar++;
scope.$apply();
});
$apply是我們的$scope(或者是direcvie里的link函數中的scope)的一個函數,調用它會強制一次$digest循環(除非當前正在執行循環,這種情況下會拋出一個異常,這是我們不需要在那里執行$apply的標志)。
更好的使用$apply的方法:
element.bind('click', function() {
scope.$apply(function() {
scope.foo++;
scope.bar++;
});
})
有什么不一樣的?差別就是在第一個版本中,我們是在angular context的外面更新的數據,如果有發生錯誤,Angular永遠不知道。很明顯在這個像個小玩具的例子里面不會出什么大錯,但是想象一下我們如果有個alert框顯示錯誤給用戶,然后我們有個第三方的庫進行一個網絡調用然后失敗了,如果我們不把它封裝進$apply里面,Angular永遠不會知道失敗了,alert框就永遠不會彈出來了。
因此,如果你想使用一個jQuery插件,並且要執行$digest循環來更新你的DOM的話,要確保你調用了$apply。
有時候我想多說一句的是有些人在不得不調用$apply時會“感覺不妙”,因為他們會覺得他們做錯了什么。其實不是這樣的,Angular不是什么魔術師,他也不知道第三方庫想要更新綁定的數據。
6.1.5使用$watch來監視
你已經知道了我們設置的任何綁定都有一個它自己的$watch,當需要時更新DOM,但是我們如果要自定義自己的watches呢?簡單,來看個例子:
/*Controller app.js */
app.controller('MainCtrl', function($scope) {
$scope.name = "Angular";
$scope.updated = -1;
$scope.$watch('name', function() {
$scope.updated++;
});
});
/*View index.html*/
<body ng-controller="MainCtrl">
<input ng-model="name" />
Name updated: {{updated}} times.
</body>
這就是我們創造一個新的$watch的方法。第一個參數是一個字符串或者函數,在這里是只是一個字符串,就是我們要監視的變量的名字,在這里,$scope.name(注意我們只需要
用name)。第二個參數是當$watch說我監視的表達式發生變化后要執行的。我們要知道的第一件事就是當controller執行到這個$watch時,它會立即執行一次,因此我們設置updated為-1。
例子2:
/*Controller app.js */
app.controller('MainCtrl', function($scope) {
$scope.name = "Angular";
$scope.updated = 0;
$scope.$watch('name', function(newValue, oldValue) {
if (newValue === oldValue) { return; } // AKA first run
$scope.updated++;
});
});
/*View index.html*/
<body ng-controller="MainCtrl">
<input ng-model="name" />
Name updated: {{updated}} times.
</body>
watch的第二個參數接受兩個參數,新值和舊值。我們可以用他們來略過第一次的執行。通常你不需要略過第一次執行,但在這個例子里面你是需要的。
例子3:
/*Controller app.js */
app.controller('MainCtrl', function($scope) {
$scope.user = { name: "Fox" };
$scope.updated = 0;
$scope.$watch('user', function(newValue, oldValue) {
if (newValue === oldValue) { return; }
$scope.updated++;
});
});
/*View index.html*/
<body ng-controller="MainCtrl">
<input ng-model="user.name" />
Name updated: {{updated}} times.
</body>
我們想要監視$scope.user對象里的任何變化,和以前一樣這里只是用一個對象來代替前面的字符串。
呃?沒用,為啥?因為$watch默認是比較兩個對象所引用的是否相同,在例子1和2里面,每次更改$scope.name都會創建一個新的基本變量,因此$watch會執行,因為對這個變量的引用已經改變了。在上面的例子里,我們在監視$scope.user,當我們改變$scope.user.name時,對$scope.user的引用是不會改變的,我們只是每次創建了一個新的$scope.user.name,但是$scope.user永遠是一樣的。
例子4:
/*Controller app.js */
app.controller('MainCtrl', function($scope) {
$scope.user = { name: "Fox" };
$scope.updated = 0;
$scope.$watch('user', function(newValue, oldValue) {
if (newValue === oldValue) { return; }
$scope.updated++;
}, true );
});
/*View index.html*/
<body ng-controller="MainCtrl">
<input ng-model="user.name" />
Name updated: {{updated}} times.
</body>
現在有用了吧!因為我們對$watch加入了第三個參數,它是一個bool類型的參數,表示的是我們比較的是對象的值而不是引用。由於當我們更新$scope.user.name時$scope.user也會改變,所以能夠正確觸發。
6.1.6 總結
我希望你們已經學會了在Angular中數據綁定是如何工作的。我猜想你的第一印象是dirty-checking很慢,好吧,其實是不對的。它像閃電般快。但是,如果你在一個模版里有2000-3000個watch,它會開始變慢。但是我覺得如果你達到這個數量級,就可以找個用戶體驗專家咨詢一下了。
無論如何,隨着ECMAScript6的到來,在Angular未來的版本里我們將會有Object.observe那樣會極大改善$digest循環的速度。
6.2自定義指令詳解
angular的指令機制。angular通過指令的方式實現了HTML的擴展,增強后的HTML不僅長相煥然一新,同時也獲得了很多強大的技能。更厲害的是,你還可以自定義指令,這就意味着HTML標簽的范圍可以擴展到無窮大。angular賦予了你造物主的能力。既然是作為angular的精華之一,相應的指令相關的知識也很多的。
6.2.1指令的編譯過程
在開始自定義指令之前,我們有必要了解一下指令在框架中的執行流程:
1.瀏覽器得到 HTML 字符串內容,解析得到 DOM 結構。
2.ng 引入,把 DOM 結構扔給 $compile 函數處理:
① 找出 DOM 結構中有變量占位符;
② 匹配找出 DOM 中包含的所有指令引用;
③ 把指令關聯到 DOM;
④ 關聯到 DOM 的多個指令按權重排列;
⑤ 執行指令中的 compile 函數(改變 DOM 結構,返回 link 函數);
⑥ 得到的所有 link 函數組成一個列表作為 $compile 函數的返回。
3. 執行 link 函數(連接模板的 scope)。
這里注意區別一下$compile和compile,前者是ng內部的編譯服務,后者是指令中的編譯函數,兩者發揮作用的范圍不同。compile和link函數息息相關又有所區別,這個在后面會講。了解執行流程對后面的理解會有幫助。
在這里有些人可能會問,angular不就是一個js框架嗎,怎么還能跟編譯扯上呢,又不是像C++那樣的高級語言。其實此編譯非彼編譯,ng編譯的工作是解析指令、綁定監聽器、替換模板中的變量等。因為工作方式很像高級語言編輯中的遞歸、堆棧過程,所以起名為編譯,不要疑惑。
6.2.2指令的使用方式及命名方法
指令的幾種使用方式如下:
- 作為標簽:<my-dir></my-dir>
- 作為屬性:<span my-dir="exp"></span>
- 作為注釋:<!-- directive: my-dir exp -->
- 作為類名:<span class="my-dir: exp;"></span>
其實常用的就是作為標簽和屬性,下面兩種用法目前還沒見過,感覺就是用來賣萌的,姑且留個印象。我們自定義的指令就是要支持這樣的用法。
關於自定義指令的命名,你可以隨便怎么起名字都行,官方是推薦用[命名空間-指令名稱]這樣的方式,像ng-controller。不過你可千萬不要用ng-前綴了,防止與系統自帶的指令重名。另外一個需知道的地方,指令命名時用駝峰規則,使用時用-分割各單詞。如:定義myDirective,使用時像這樣:<my-directive>。
6.2.3自定義指令的配置參數
下面是定義一個標准指令的示例,可配置的參數包括以下部分:
myModule.directive('namespaceDirectiveName', function factory(injectables) {
var directiveDefinitionObject = {
restrict: string,//指令的使用方式,包括標簽,屬性,類,注釋
priority: number,//指令執行的優先級
template: string,//指令使用的模板,用HTML字符串的形式表示
templateUrl: string,//從指定的url地址加載模板
replace: bool,//是否用模板替換當前元素,若為false,則append在當前元素上
transclude: bool,//是否將當前元素的內容轉移到模板中
scope: bool or object,//指定指令的作用域
controller: function controllerConstructor($scope, $element, $attrs, $transclude){...},//定義與其他指令進行交互的接口函數
require: string,//指定需要依賴的其他指令
link: function postLink(scope, iElement, iAttrs) {...},//以編程的方式操作DOM,包
括添加監聽器等
compile: function compile(tElement, tAttrs, transclude){
return: {
pre: function preLink(scope, iElement, iAttrs, controller){...},
post: function postLink(scope, iElement, iAttrs, controller){...}
}
}//編程的方式修改DOM模板的副本,可以返回鏈接函數
};
return directiveDefinitionObject;
});
看上去好復雜的樣子,定義一個指令需要這么多步驟嘛?當然不是,你可以根據自己的需要來選擇使用哪些參數。事實上priority和compile用的比較少,template和templateUrl又是互斥的,兩者選其一即可。所以不必緊張,接下來分別學習一下這些參數:
l 指令的表現配置參數:restrict、template、templateUrl、replace、transclude;
l 指令的行為配置參數:compile和link;
l 指令划分作用域配置參數:scope;
l 指令間通信配置參數:controller和require。
6.2.3指令的表現參數restrict等
指令的表現配置參數:restrict、template、templateUrl、replace、transclude。
我將先從一個簡單的例子開始。
例子的代碼如下:
var app = angular.module('MyApp', [], function(){console.log('here')});
app.directive('sayHello',function(){
return {
restrict : 'E',
template : '<div>hello</div>'
};
})
然后在頁面中,我們就可以使用這個名為sayHello的指令了,它的作用就是輸出一個hello單詞。像這樣使用:
<say-hello></say-hello>
這樣頁面就會顯示出hello了,看一下生成的代碼:
<say-hello>
<div>hello</div>
</say-hello>
稍稍解釋一下我們用到的兩個參數,restirct用來指定指令的使用類型,其取值及含義如下:
取值 |
含義 |
使用示例 |
E |
標簽 |
<my-menu title=Products></my-menu> |
A |
屬性 |
<div my-menu=Products></div> |
C |
類 |
<div class="my-menu":Products></div> |
M |
注釋 |
<!--directive:my-menu Products--> |
默認值是A。也可以使用這些值的組合,如EA,EC等等。我們這里指定為E,那么它就可以像標簽一樣使用了。如果指定為A,我們使用起來應該像這樣:
<div say-hello></div>
從生成的代碼中,你也看到了template的作用,它就是描述你的指令長什么樣子,這部分內容將出現在頁面中,即該指令所在的模板中,既然是模板中,template的內容中也可以使用ng-modle等其他指令,就像在模板中使用一樣。
在上面生成的代碼中,我們看到了<div>hello</div>外面還包着一層<say-hello>標簽,如果我們不想要這一層多余的東西了,replace就派上用場了,在配置中將replace賦值為true,將得到如下結構:
<div>hello</div>
replace的作用正如其名,將指令標簽替換為了temple中定義的內容。不寫的話默認為false。
上面的template未免也太簡單了,如果你的模板HTML較復雜,如自定義一個ui組件指令,難道要拼接老長的字符串?當然不需要,此時只需用templateUrl便可解決問題。你可以將指令的模板單獨命名為一個html文件,然后在指令定義中使用templateUrl指定好文件的路徑即可,如:
templateUrl : ‘helloTemplate.html’
系統會自動發一個http請求來獲取到對應的模板內容。是不是很方便呢,你不用糾結於拼接字符串的煩惱了。如果你是一個追求完美的有考慮性能的工程師,可能會發問:那這樣的話豈不是要犧牲一個http請求?這也不用擔心,因為ng的模板還可以用另外一種方式定義,那就是使用<script>標簽。使用起來如下:
<script type="text/ng-template" id="helloTemplate.html">
<div>hello</div>
</script>
你可以把這段代碼寫在頁面頭部,這樣就不必去請求它了。在實際項目中,你也可以將所有的模板內容集中在一個文件中,只加載一次,然后根據id來取用。
接下來我們來看另一個比較有用的配置:transclude,定義是否將當前元素的內容轉移到模板中。看解釋有點抽象,不過親手試試就很清楚了,看下面的代碼(例06):
app.directive('sayHello',function(){
return {
restrict : 'E',
template : '<div>hello,<b ng-transclude></b>!</div>',
replace : true,
transclude : true
};
})
指定了transclude為true,並且template修改了一下,加了一個<b>標簽,並在上面使用了ng-transclude指令,用來告訴指令把內容轉移到的位置。那我們要轉移的內容是什么呢?請看使用指令時的變化:
<say-hello>美女</say-hello>
內容是什么你也看到了哈~在運行的時候,美女將會被轉移到<b>標簽中,原來此配置的作用就是——乾坤大挪移!看效果:
hello, 美女!
這個還是很有用的,因為你定義的指令不可能老是那么簡單,只有一個空標簽。當你需要對指令中的內容進行處理時,此參數便大有可用。
6.2.4指令的行為參數:compile和link
6.2.3中簡單介紹了自定義一個指令的幾個簡單參數,restrict、template、templateUrl、replace、transclude,這幾個理解起來相對容易很多,因為它們只涉及到了表現,而沒有涉及行為。我們繼續學習ng自定義指令的幾個重量級參數:compile和link
l 理解compile和link
不知大家有沒有這樣的感覺,自己定義指令的時候跟寫jQuery插件有幾分相似之處,都是先預先定義好頁面結構及監聽函數,然后在某個元素上調用一下,該元素便擁有了特殊的功能。區別在於,jQuery的側重點是DOM操作,而ng的指令中除了可以進行DOM操作外,更注重的是數據和模板的綁定。jQuery插件在調用的時候才開始初始化,而ng指令在頁面加載進來的時候就被編譯服務($compile)初始化好了。
在指令定義對象中,有compile和link兩個參數,它們是做什么的呢?從字面意義上看,編譯、鏈接,貌似太抽象了點。其實可大有內涵,為了在自定義指令的時候能正確使用它們,現在有必要了解一下ng是如何編譯指令的。
l 指令的解析流程詳解
我們知道ng框架會在頁面載入完畢的時候,根據ng-app划定的作用域來調用$compile服務進行編譯,這個$compile就像一個大總管一樣,清點作用域內的DOM元素,看看哪些元素上使用了指令(如<div ng-modle=”m”></div>),或者哪些元素本身就是個指令(如<mydierc></mydirec>),或者使用了插值指令( {{}}也是一種指令,叫interpolation directive),$compile大總管會把清點好的財產做一個清單,然后根據這些指令的優先級(priority)排列一下,真是個細心的大總管哈~大總管還會根據指令中的配置參數(template,place,transclude等)轉換DOM,讓指令“初具人形”。
然后就開始按順序執行各指令的compile函數,注意此處的compile可不是大總管$compile,人家帶着$是土豪,此處執行的compile函數是我們指令中配置的,compile函數中可以訪問到DOM節點並進行操作,其主要職責就是進行DOM轉換,每個compile函數執行完后都會返回一個link函數,這些link函數會被大總管匯合一下組合成一個合體后的link函數,為了好理解,我們可以把它想象成葫蘆小金剛,就像是進行了這樣的處理。
//合體后的link函數
function AB(){
A(); //子link函數
B(); //子link函數
}
接下來進入link階段,合體后的link函數被執行。所謂的鏈接,就是把view和scope鏈接起來。鏈接成啥樣呢?就是我們熟悉的數據綁定,通過在DOM上注冊監聽器來動態修改scope中的數據,或者是使用$watchs監聽 scope中的變量來修改DOM,從而建立雙向綁定。由此也可以斷定,葫蘆小金剛可以訪問到scope和DOM節點。
不要忘了我們在定義指令中還配置着一個link參數呢,這么多link千萬別搞混了。那這
個link函數是干嘛的呢,我們不是有葫蘆小金剛了嘛?那我告訴你,其實它是一個小三。此話怎講?compile函數執行后返回link函數,但若沒有配置compile函數呢?葫蘆小金剛自然就不存在了。
正房不在了,當然就輪到小三出馬了,大總管$compile就把這里的link函數拿來執行。這就意味着,配置的link函數也可以訪問到scope以及DOM節點。值得注意的是,compile函數通常是不會被配置的,因為我們定義一個指令的時候,大部分情況不會通過編程的方式進行DOM操作,而更多的是進行監聽器的注冊、數據的綁定。所以,小三名正言順的被大總管寵愛。
聽完了大總管、葫蘆小金剛和小三的故事,你是不是對指令的解析過程比較清晰了呢?不過細細推敲,你可能還是會覺得情節生硬,有些細節似乎還是沒有透徹的明白,所以還需要再理解下面的知識點:
l compile和link的區別
其實在我看完官方文檔后就一直有疑問,為什么監聽器、數據綁定不能放在compile函數中,而偏偏要放在link函數中?為什么有了compile還需要link?就跟你質疑我編的故事一樣,為什么最后小三被寵愛了?所以我們有必要探究一下,compile和link之間到底有什么區別。好,正房與小三的PK現在開始。
首先是性能。舉個例子:
<ul>
<li ng-repeat="a in array">
<input ng-modle=”a.m” />
</li>
</ul>
我們的觀察目標是ng-repeat指令。假設一個前提是不存在link。大總管$compile在編譯這段代碼時,會查找到ng-repeat,然后執行它的compile函數,compile函數根據array的長度復制出n個<li>標簽。而復制出的<li>節點中還有<input>節點並且使用了ng-modle指令,所以compile還要掃描它並匹配指令,然后綁定監聽器。每次循環都做如此多的工作。而更加糟糕的一點是,我們會在程序中向array中添加元素,此時頁面上會實時更新DOM,每次有新元素進來,compile函數都把上面的步驟再走一遍,豈不是要累死了,這樣性能必然不行。
現在扔掉那個假設,在編譯的時候compile就只管生成DOM的事,碰到需要綁定監聽器的地方先存着,有幾個存幾個,最后把它們匯總成一個link函數,然后一並執行。這樣就輕松多了,compile只需要執行一次,性能自然提升。
另外一個區別是能力。
盡管compile和link所做的事情差不多,但它們的能力范圍還是不一樣的。比如正房能管你的存款,小三就不能。小三能給你初戀的感覺,正房卻不能。
我們需要看一下compile函數和link函數的定義:
function compile(tElement, tAttrs, transclude) { ... }
function link(scope, iElement, iAttrs, controller) { ... }
這些參數都是通過依賴注入而得到的,可以按需聲明使用。從名字也容易看出,兩個函數各自的職責是什么,compile可以拿到transclude,允許你自己編程管理乾坤大挪移的行為。而link中可以拿到scope和controller,可以與scope進行數據綁定,與其他指令進行通信。兩者雖然都可以拿到element,但是還是有區別的,看到各自的前綴了吧?compile拿到的是編譯前的,是從template里拿過來的,而link拿到的是編譯后的,已經與作用域建立了
關聯,這也正是link中可以進行數據綁定的原因。
我暫時只能理解到這個程度了。實在不想理解這些知識的話,只要簡單記住一個原則就行了:如果指令只進行DOM的修改,不進行數據綁定,那么配置在compile函數中,如果指令要進行數據綁定,那么配置在link函數中。
6.2.5指令的划分作用域參數:scope
我們在上面寫了一個簡單的<say-hello></say-hello>,能夠跟美女打招呼。但是看看人家ng內置的指令,都是這么用的:ng-model=”m”,ng-repeat=”a in array”,不單單是作為屬性,還可以賦值給它,與作用域中的一個變量綁定好,內容就可以動態變化了。假如我們的sayHello可以這樣用:<say-hello speak=”content”>美女</say-hello>,把要對美女說的話寫在一個變量content中,然后只要在controller中修改content的值,頁面就可以顯示對美女說的不同的話。這樣就靈活多了,不至於見了美女只會說一句hello,然后就沒有然后。
為了實現這樣的功能,我們需要使用scope參數,下面來介紹一下。
使用scope為指令划分作用域
顧名思義,scope肯定是跟作用域有關的一個參數,它的作用是描述指令與父作用域的關系,這個父作用域是指什么呢?想象一下我們使用指令的場景,頁面結構應該是這個樣子:
<div ng-controller="testC">
<say-hello speak="content">美女</say-hello>
</div>
外層肯定會有一個controller,而在controller的定義中大體是這個樣子:
var app = angular.module('MyApp', [], function(){console.log('here')});
app.controller('testC',function($scope){
$scope.content = '今天天氣真好!';
});
所謂sayHello的父作用域就是這個名叫testC的控制器所管轄的范圍,指令與父作用域的關系可以有如下取值:
取值 |
說明 |
false |
默認值。使用父作用域作為自己的作用域 |
true |
新建一個作用域,該作用域繼承父作用域 |
javascript對象 |
與父作用域隔離,並指定可以從父作用域訪問的變量 |
乍一看取值為false和true好像沒什么區別,因為取值為true時會繼承父作用域,即父作用域中的任何變量都可以訪問到,效果跟直接使用父作用域差不多。但細細一想還是有區別的,有了自己的作用域后就可以在里面定義自己的東西,與跟父作用域混在一起是有本質上的區別。好比是父親的錢你想花多少花多少,可你自己掙的錢父親能花多少就不好說了。你若想看這兩個作用域的區別,可以在link函數中打印出來看看,還記得link函數中可以訪問到scope吧。
最有用的還是取值為第三種,一個對象,可以用鍵值來顯式的指明要從父作用域中使用屬性的方式。當scope值為一個對象時,我們便建立了一個與父層隔離的作用域,不過也不是完全隔離,我們可以手工搭一座橋梁,並放行某些參數。我們要實現對美女說各種話就得靠這個。使用起來像這樣:
scope: {
attributeName1: 'BINDING_STRATEGY',
attributeName2: 'BINDING_STRATEGY',...
}
鍵為屬性名稱,值為綁定策略。等等!啥叫綁定策略?最討厭冒新名詞卻不解釋的行為!別急,聽我慢慢道來。
先說屬性名稱吧,你是不是認為這個attributeName1就是父作用域中的某個變量名稱?錯!其實這個屬性名稱是指令自己的模板中要使用的一個名稱,並不對應父作用域中的變量,稍后的例子中我們來說明。再來看綁定策略,它的取值按照如下的規則:
符號 |
說明 |
舉例 |
@ |
傳遞一個字符串作為屬性的值 |
str : ‘@string’ |
= |
使用父作用域中的一個屬性,綁定數據到指令的屬性中 |
name : ‘=username’ |
& |
使用父作用域中的一個函數,可以在指令中調用 |
getName : ‘&getUserName’ |
總之就是用符號前綴來說明如何為指令傳值。你肯定迫不及待要看例子了,我們結合例子看一下,小二,上栗子~
舉例說明
我想要實現上面想像的跟美女多說點話的功能,即我們給sayHello指令加一個屬性,通過給屬性賦值來動態改變說話的內容 主要代碼如下:
app.controller('testC',function($scope){
$scope.content = '今天天氣真好!';
});
app.directive('sayHello',function(){
return {
restrict : 'E',
template: '<div>hello,<b ng-transclude></b>,{{ cont }}</div>',
replace : true,
transclude : true,
scope : {
cont : '=speak'
}
};
});
然后在模板中,我們如下使用指令:
<div ng-controller="testC">
<say-hello speak=" content ">美女</say-hello>
</div>
看看運行效果:
美女今天天氣真好!
執行的流程是這樣的:
① 指令被編譯的時候會掃描到template中的{ {cont} },發現是一個表達式;
② 查找scope中的規則:通過speak與父作用域綁定,方式是傳遞父作用域中的屬性;
③ speak與父作用域中的content屬性綁定,找到它的值“今天天氣真好!”;
④ 將content的值顯示在模板中。
這樣我們說話的內容content就跟父作用域綁定到了一其,如果動態修改父作用域的content的值,頁面上的內容就會跟着改變,正如你點擊“換句話”所看到的一樣。
這個例子也太小兒科了吧!簡單雖簡單,但可以讓我們理解清楚,為了檢驗你是不是真的明白了,可以思考一下如何修改指令定義,能讓sayHello以如下兩種方式使用:
<span say-hello speak="content">美女</span>
<span say-hello="content" >美女</span>
答案我就不說了,簡單的很。下面有更重要的事情要做,我們說好了要寫一個真正能用的東西來着。接下來就結合所學到的東西來寫一個折疊菜單,即點擊可展開,再點擊一次就收縮回去的菜單。
控制器及指令的代碼如下(例07):
app.controller('testC',function($scope){
$scope.title = '個人簡介';
$scope.text = '大家好,我是一名前端工程師,我正在研究AngularJs,歡迎大家與我交流';
});
app.directive('expander',function(){
return {
restrict : 'E',
templateUrl : 'expanderTemp.html',
replace : true,
transclude : true,
scope : {
mytitle : '=etitle'
},
link : function(scope,element,attris){
scope.showText = false;
scope.toggleText = function(){
scope.showText = ! scope.showText;
}
}
};
});
HTML中的代碼如下:
<script type="text/ng-template" id="expanderTemp.html">
<div class="mybox">
<div class="mytitle" ng-click="toggleText()">
{{mytitle}}
</div>
<div ng-transclude ng-show="showText">
</div>
</div>
</script>
<div ng-controller="testC">
<expander etitle="title">{{text}}</expander>
</div>
還是比較容易看懂的,我只做一點必要的解釋。首先我們定義模板的時候使用了ng的一種定義方式<script type=”text/ng-template”id="expanderTemp.html">,在指令中就可以用templateUrl根據這個id來找到模板。指令中的{{mytitle}}表達式由scope參數指定從etitle傳遞,etitle指向了父作用域中的title。為了實現點擊標題能夠展開收縮內容,我們把這部分邏輯放在了link函數中,link函數可以訪問到指令的作用域,我們定義showText屬性來表示內容部分的顯隱,定義toggleText函數來進行控制,然后在模板中綁定好。 如果把showText和toggleText定義在controller中,作為$scope的屬性呢?顯然是不行的,這就是隔離作用域的意義所在,父作用域中的東西除了title之外通通被屏蔽。
上面的例子中,scope參數使用了=號來指定獲取屬性的類型為父作用域的屬性,如果我們想在指令中使用父作用域中的函數,使用&符號即可,是同樣的原理。
6.2.6指令間通信參數:controller和require
使用指令來定義一個ui組件是個不錯的想法,首先使用起來方便,只需要一個標簽或者屬性就可以了,其次是可復用性高,通過controller可以動態控制ui組件的內容,而且擁有雙向綁定的能力。當我們想做的組件稍微復雜一點,就不是一個指令可以搞定的了,就需要指令與指令的協作才可以完成,這就需要進行指令間通信。
想一下我們進行模塊化開發的時候的原理,一個模塊暴露(exports)對外的接口,另外一個模塊引用(require)它,便可以使用它所提供的服務了。ng的指令間協作也是這個原理,這也正是自定義指令時controller參數和require參數的作用。
controller參數用於定義指令對外提供的接口,它的寫法如下:
controller: function controllerConstructor($scope, $element, $attrs, $transclude)
它是一個構造器函數,將來可以構造出一個實例傳給引用它的指令。為什么叫controller(控制器)呢?其實就是告訴引用它的指令,你可以控制我。至於可以控制那些東西呢,就需要在函數體中進行定義了。先看controller可以使用的參數,作用域、節點、節點的屬性、節點內容的遷移,這些都可以通過依賴注入被傳進來,所以你可以根據需要只寫要用的參數。關於如何對外暴露接口,我們在下面的例子來說明。
require參數便是用來指明需要依賴的其他指令,它的值是一個字符串,就是所依賴的指令的名字,這樣框架就能按照你指定的名字來從對應的指令上面尋找定義好的controller了。不過還稍稍有點特別的地方,為了讓框架尋找的時候更輕松些,我們可以在名字前面加個小小的前綴:^,表示從父節點上尋找,使用起來像這樣:require : ‘^directiveName’,如果不加,$compile服務只會從節點本身尋找。另外還可以使用前綴:?,此前綴將告訴$compile服務,如果所需的controller沒找到,不要拋出異常。
所需要了解的知識點就這些,接下來是例子時間,依舊是從書上抄來的一個例子,我們要做的是一個手風琴菜單,就是多個折疊菜單並列在一起,此例子用來展示指令間的通信再合適不過。
首先我們需要定義外層的一個結構,起名為accordion,代碼如下:
app.directive('accordion',function(){
return {
restrict : 'E',
template : '<div ng-transclude></div>',
replace : true,
transclude : true,
controller :function(){
var expanders = [];
this.gotOpended = function(selectedExpander){
angular.forEach(expanders,function(e){
if(selectedExpander != e){
e.showText = false;
}
});
}
this.addExpander = function(e){
expanders.push(e);
}
}
}
});
需要解釋的只有controller中的代碼,我們定義了一個折疊菜單數組expanders,並且通過this關鍵字來對外暴露接口,提供兩個方法。gotOpended接受一個selectExpander參數用來修改數組中對應expander的showText屬性值,從而實現對各個子菜單的顯隱控制。addExpander方法對外提供向expanders數組增加元素的接口,這樣在子菜單的指令中,便可以調用它把自身加入到accordion中。
看一下我們的expander需要做怎樣的修改呢:
app.directive('expander',function(){
return {
restrict : 'E',
templateUrl : 'expanderTemp.html',
replace : true,
transclude : true,
require : '^?accordion',
scope : {
title : '=etitle'
},
link : function(scope,element,attris,accordionController){
scope.showText = false;
accordionController.addExpander(scope);
scope.toggleText = function(){
scope.showText = ! scope.showText;
accordionController.gotOpended(scope);
}
}
};
});
首先使用require參數引入所需的accordion指令,添加?^前綴表示從父節點查找並且失敗后不拋出異常。然后便可以在link函數中使用已經注入好的accordionController了,調用addExpander方法將自己的作用域作為參數傳入,以供accordionController訪問其屬性。然
后在toggleText方法中,除了要把自己的showText修改以外,還要調用accordionController的gotOpended方法通知父層指令把其他菜單給收縮起來。
指令定義好后,我們就可以使用了,使用起來如下:
<accordion>
<expander ng-repeat="expander in expanders" etitle="expander.title">
{{expander.text}}
</expander>
</accordion>
外層使用了accordion指令,內層使用expander指令,並且在expander上用ng-repeat循環輸出子菜單。請注意這里遍歷的數組expanders可不是accordion中定義的那個expanders,如果你這么認為了,說明還是對作用域不夠了解。此expanders是ng-repeat的值,它是在外層controller中的,所以,在testC中,我們需要添加如下數據:
$scope.expanders = [
{title: '個人簡介',
text: '大家好,我是一名前端工程師,我正在研究AngularJs,歡迎大家與我交流'},
{title: '我的愛好',
text: 'LOL '},
{title: '性格',
text: ' 我的性格就是無性格'}
];
6.3 性能及調優
6.3.1性能測試
AnglarJS作為一款優秀的Web框架,可大大簡化前端開發的負擔。
AnglarJS很棒,但當處理包含復雜數據結構的大型列表時,其運行速度就會非常慢。
這是我們將核心管理頁面遷移到AngularJS過程中遇到的問題。這些頁面在顯示500行數據時本應該工作順暢,但首個方法的渲染時間竟花費了7秒,太可怕了。后來,我們發現了在實現過程中存在兩個主要性能問題。一個與“ng-repeat ”指令有關,另一個與過濾器有關。
AngularJS 中的ng-repeat在處理大型列表時,速度為什么會變慢?
AngularJS中的ng-repeat在處理2500個以上的雙向數據綁定時速度會變慢。這是由於AngularJS通過“dirty checking”函數來檢測變化。每次檢測都會花費時間,所以包含復雜數據結構的大型列表將降低你應用的運行速度。
提高性能的先決條件
時間記錄指令
為了測量一個列表渲染所花費的時間,我們寫了一個簡單的程序,通過使用“ng-repeat”的屬性“$last”來記錄時間。時間存放在TimeTracker服務中,這樣時間記錄就與服務器端的數據加載分開了。
// Post repeat directive for logging the rendering time
angular.module('siApp.services').directive('postRepeatDirective',
['$timeout', '$log', 'TimeTracker',
function($timeout, $log, TimeTracker) {
return function(scope, element, attrs) {
if (scope.$last){
$timeout(function(){
var timeFinishedLoadingList = TimeTracker.reviewListLoaded();
var ref = new Date(timeFinishedLoadingList);
var end = new Date();
$log.debug("## DOM rendering list took: " + (end - ref) + " ms");
});
}
};
}
]);
// Use in HTML:
<tr ng-repeat="item in items" post-repeat-directive>…</tr>
Chrome開發者工具的時間軸(Timeline)屬性
在Chrome開發者工具的時間軸標簽中,你可以看見事件、每秒內瀏覽器幀數和內存分配。“memory”工具用來檢測內存泄漏,及頁面所需的內存。當幀速率每秒低於30幀時就會出現頁面閃爍問題。“frames”工具可幫助了解渲染性能,還可顯示出一個JavaScript任務所花費的CPU時間。
通過限制列表的大小進行基本的調優
緩解該問題,最好的辦法是限制所顯示列表的大小。可通過分頁、添加無限滾動條來實現。
分頁,我們可以使用AngularJS的“limitTo”過濾器(AngularJS1.1.4版本以后)和“startFrom”過濾器。可以通過限制顯示列表的大小來減少渲染時間。這是減少渲染時間最高效的方法。
6.3.2七大調優法則
1.渲染沒有數據綁定的列表
這是最明顯的解決方案,因為數據綁定是性能問題最可能的根源。如果你只想顯示一次列表,並不需要更新、改變數據,放棄數據綁定是絕佳的辦法。不過可惜的是,你會失去對數據的控制權,但除了該法,我們別無選擇。
2.不要使用內聯方法計算數據
為了在控制器中直接過濾列表,不要使用可獲得過濾鏈接的方法。“ng-repeat”會評估每個表達式。在我們的案例中,“filteredItems()”返回過濾鏈接。如果評估過程很慢,它將迅速降低整個應用的速度。
l <li ng-repeat="item in filteredItems()"> //這並不是一個好方法,因為要頻繁地評估。
l <li ng-repeat="item in items"> //這是要采用的方法
3.使用兩個列表(一個用來進行視圖顯示,一個作為數據源)
將要顯示的列表與總的數據列表分開,是非常有用的模型。你可以對一些過濾進行預處理,並將存於緩存中的鏈接應用到視圖上。下面案例展示了基本實現過程。filteredLists變量保存着緩存中的鏈接,applyFilter方法來處理映射。
/* Controller */
// Basic list
var items = [{name:"John", active:true }, {name:"Adam"}, {name:"Chris"}, {name:"Heather"}];
// Init displayedList
$scope.displayedItems = items;
// Filter Cache
var filteredLists['active'] = $filter('filter)(items, {"active" : true});
// Apply the filter
$scope.applyFilter = function(type) {
if (filteredLists.hasOwnProperty(type){ // Check if filter is cached
$scope.displayedItems = filteredLists[type];
} else {
/* Non cached filtering */
}
}
// Reset filter
$scope.resetFilter = function() {
$scope.displayedItems = items;
}
/* View */
<button ng-click="applyFilter('active')">Select active</button>
<ul><li ng-repeat="item in displayedItems">{{item.name}}<li></ul>
4.在其他模板中使用ng-if來代替ng-show
如果你用指令、模板來渲染額外的信息,例如通過點擊來顯示列表項的詳細信息,一定要使用 ng-if(AngularJSv. 1.1.5以后)。ng-if可阻止渲染(與ng-show相比)。所以其它DOM和數據綁定可根據需要進行評估。
<li ng-repeat="item in items">
<p> {{ item.title }} </p>
<button ng-click="item.showDetails = !item.showDetails">Show details</buttons>
<div ng-if="item.showDetails">
{{item.details}}
</div>
</li>
5.不要使用ng-mouseenter、ng-mouseleave等指令
使用內部指令,像ng-mouseenter,AngularJS會使你的頁面閃爍。瀏覽器的幀速率通常低於每秒30幀。使用jQuery創建動畫、鼠標懸浮效果可以解決該問題。確保將鼠標事件放入jQuery的.live()函數中。
6.關於過濾的小提示:通過ng-show隱藏多余的元素
對於長列表,使用過濾同樣會減低工作效率,因為每個過濾都會創建一個原始列表的子鏈接。在很多情況下,數據沒有變化,過濾結果也會保持不變。所以對數據列表進行預過濾,並根據情況將它應用到視圖中,會大大節約處理時間。
在ng-repeat指令中使用過濾器,每個過濾器會返回一個原始鏈接的子集。AngularJS 從DOM中移除多余元素(通過調用 $destroy),同時也會從$scope中移除他們。當過濾器的輸入發生改變時,子集也會隨着變化,元素必須進行重新鏈接,或着再調用$destroy。
大部分情況下,這樣做很好,但一旦用戶經常過濾,或者列表非常巨大,不斷的鏈接與
銷毀將影響性能。為了加快過濾的速度,你可以使用ng-show和ng-hide指令。在控制器中,進行過濾,並為每項添加一個屬性。依靠該屬性來觸發ng-show。結果是,只為這些元素增加ng-hide類,來代替將它們移除子列表、$scope和DOM。
觸發ng-show的方法之一是使用表達式語法。ng-show的值由表達式語法來確定。可以看下面的例子:
<input ng-model="query"></input>
<li ng-repeat="item in items" ng-show="([item.name] | filter:query).length"> {{item.name}} </li>
<span style="font-size: 14px; line-height: 24px; font-family:; white-space: normal;"></span>
7.關於過濾的小提示:防抖動輸入
解決第6點提出的持續過濾問題的另一個方法是防抖動用戶輸入。例如,如果用戶輸入一個搜索關鍵詞,只當用戶停止輸入后,過濾器才會被激活。使用該防抖動服務的一個很好的解決方案請見: http://jsfiddle.net/Warspawn/6K7Kd/。將它應用到你的視圖及控制器中,如下所示:
/* Controller */
// Watch the queryInput and debounce the filtering by 350 ms.
$scope.$watch('queryInput', function(newValue, oldValue) {
if (newValue === oldValue) { return; }
$debounce(applyQuery, 350);
});
var applyQuery = function() {
$scope.filter.query = $scope.query;
};
/* View */
<input ng-model="queryInput"/>
<li ng-repeat= item in items | filter:filter.query>{{ item.title }} </li>
7 總結
angular上手比較難,初學者(特別是習慣了使用JQuery的人)可能不太適應其語法以及思想。隨着對ng探索的一步步深入,也確實感覺到了這一點,尤其是框架內部的某些執行機制。
7.1頁面效果
ng-show ng-hide 無動畫效果問題
7.2委派事件(代理事件)
7.2.1 NG循環及事件綁定
<ul>
<li ng-repeat="a in array">
<input ng-modle=”a.m” />
</li>
</ul>
Ng會根據array的長度復制出n個<li>標簽。而復制出的<li>節點中還有<input>節點並且使用了ng-modle指令,所以ng會對所有的<input>綁定監聽器(事件)。如果array很大,就會綁定太多的事件,性能出現問題。
7.2.2 jQuery委派事件
從jQuery1.7開始,提供了.on()附加事件處理程序。
.on( events [, selector ] [, data ], handler(eventObject) )
參數Selector為一個選擇器字符串,用於過濾出被選中的元素中能觸發事件的后代元素。如果選擇器是 null 或者忽略了該選擇器,那么被選中的元素總是能觸發事件。
如果省略selector或者是null,那么事件處理程序被稱為直接事件 或者 直接綁定事件 。每次選中的元素觸發事件時,就會執行處理程序,不管它直接綁定在元素上,還是從后代(內部)元素冒泡到該元素的。
當提供selector參數時,事件處理程序是指為委派事件(代理事件)。事件不會在直接綁定的元素上觸發,但當selector參數選擇器匹配到后代(內部元素)的時候,事件處理函數才會被觸發。jQuery 會從 event target 開始向上層元素(例如,由最內層元素到最外層元素)開始冒泡,並且在傳播路徑上所有綁定了相同事件的元素若滿足匹配的選擇器,那么這些元素上的事件也會被觸發。
委托事件有兩個優勢:他們能在后代元素添加到文檔后,可以處理這些事件;代理事件的另一個好處就是,當需要監視很多元素的時候,代理事件的開銷更小。
例如,在一個表格的 tbody 中含有 1,000 行,下面這個例子會為這 1,000 元素綁定事
$("#dataTable tbody tr").on("click", function(event){ alert($(this).text());});
委派事件的方法只有一個元素的事件處理程序,tbody,並且事件只會向上冒泡一層(從被點擊的tr 到 tbody ):
$("#dataTable tbody").on("click", "tr", function(event){ alert($(this).text());});
許多委派的事件處理程序綁定到 document 樹的頂層附近,可以降低性能。每次發生事件時,jQuery 需要比較從 event target(目標元素) 開始到文檔頂部的路徑中每一個元素上所有該類型的事件。為了獲得更好的性能,在綁定代理事件時,綁定的元素最好盡可能的靠近目標元素。避免在大型文檔中,過多的在 document 或 document.body 上添加代理事件。