*這篇文章是轉來的,作了本身的一點修改,排版。原始出處不明,如涉及原博主版權問題,請及時告知,我將會當即刪除*。javascript
前端技術的發展是如此之快,各類優秀技術、優秀框架的出現簡直讓人應接不暇,緊跟時代潮流,學習掌握新知識天然是不敢怠慢。css
AngularJS是google在維護,其在國外已經十分火熱,但是國內的使用狀況卻有不小的差距,參考文獻/網絡文章也很匱乏。這裏便將我學習AngularJS寫成文檔,一方面做爲本身學習路程上的記錄,另外一方面也給有興趣的同窗一些參考。html
首先我本身也是一名學習者,會以學習者的角度來整理個人行文思路,這裏可能只是些探索,有理解或是技術上的錯誤還請你們指出;其次我特別喜歡編寫小例子來把一件事情說明白,故在文中會盡量多的用示例加代碼講解,我相信這會是一種比較好的方式;最後,我深知AngularJS的使用方式跟jquery的使用方式有很大不一樣,在你們都有jquery、ext經驗的條件下對於angular的學習會困難重重,不過我更相信在你們的堅持下,可以快速的學好AngularJS,至少咱也能深刻了解到AngularJS的基本思想,對我們之後本身的插件開發、項目開發都會有很大的啓示。前端
AngularJs(後面就簡稱ng了)是一個用於設計動態web應用的結構框架。首先,它是一個框架,不是類庫,是像EXT同樣提供一整套方案用於設計web應用。它不只僅是一個javascript框架,由於它的核心實際上是對HTML標籤的加強。java
何爲HTML標籤加強?其實就是使你可以用標籤完成一部分頁面邏輯,具體方式就是經過自定義標籤、自定義屬性等,這些HTML原生沒有的標籤/屬性在ng中有一個名字:指令(directive)。後面會詳細介紹。那麼,什麼又是動態web應用呢?與傳統web系統相區別,web應用能爲用戶提供豐富的操做,可以隨用戶操做不斷更新視圖而不進行url跳轉。ng官方也聲明它更適用於開發CRUD應用,即數據操做比較多的應用,而非是遊戲或圖像處理類應用。jquery
爲了實現這些,ng引入了一些很是棒的特性,包括模板機制、數據綁定、模塊、指令、依賴注入、路由。經過數據與模板的綁定,可以讓咱們擺脫繁瑣的DOM操做,而將注意力集中在業務邏輯上。git
另一個疑問,ng是MVC框架嗎?仍是MVVM框架?官網有提到ng的設計採用了MVC的基本思想,而又不徹底是MVC,由於在書寫代碼時咱們確實是在用ng-controller這個指令(起碼從名字上看,是MVC吧),但這個controller處理的業務基本上都是與view進行交互,這麼看來又很接近MVVM。讓咱們把目光移到官網那個非醒目的title上:「AngularJS — Superheroic JavaScript MVW Framework」。angularjs
AngularJS 從新定義了前端應用的開發方式。面對HTML和JavaScript之間的界線,它非但不畏縮不前,反而正面出擊,提出了有效的解決方案。github
不少前端應用的開發框架,好比Backbone、EmberJS等,都要求開發者繼承此框架特有的一些JavaScript對象。這種方式有其長處,但它沒必要要地污染了開發者本身代碼的對象空間,還要求開發者去了解內存裏那些抽象對象。儘管如此咱們仍是接受了這種方式,由於網絡最初的設計沒法提供咱們今天所需的交互性,因而咱們須要框架,來幫咱們填補JavaScript和HTML之間的鴻溝。並且有了它,你不用再「直接」操控DOM,只要給你的DOM註上metadata(即AngularJS裏的directive們),而後讓AngularJS來幫你操縱DOM。同時,AngularJS不依賴(也不妨礙)任何其餘的框架。你甚至能夠基於其它的框架來開發AngularJS應用。web
API地址:http://docs.angularjs.org/api/;
AngularJS入門教程:https://github.com/zensh/AngularjsTutorial_cn;
AngularJS開發指南:https://github.com/MaxSherry/Angularjs-Developer-Guide;
AngularJS是一個 MV* 框架,最適於開發客戶端的單頁面應用。它不是個功能庫,而是用來開發動態網頁的框架。它專一於擴展HTML的功能,提供動態數據綁定(data binding),並且它能跟其它框架(如jQuery)合做融洽。
若是你要開發的是單頁應用,AngularJS就是你的上上之選。Gmail、Google Docs、Twitter和Facebook這樣的應用,都很能發揮AngularJS的長處。可是像遊戲開發之類對DOM進行大量操縱、又或者單純須要極高運行速度的應用,就不是AngularJS的用武之地了。
AngularJS是一個新出現的強大客戶端技術,提供給你們的一種開發強大應用的方式。這種方式利用而且擴展HTML,CSS和javascript,而且彌補了它們的一些很是明顯的不足。本應該使用HTML來實現而如今由它開發的動態一些內容。
AngularJS有五個最重要的功能和特性:
數據綁定多是AngularJS最酷最實用的特性。它可以幫助你避免書寫大量的初始代碼從而節約開發時間。一個典型的web應用可能包含了80%的代碼用來處理,查詢和監聽DOM。數據綁定是的代碼更少,你能夠專一於你的應用。
咱們想象一下Model是你的應用中的簡單數據。你的Model是你用來讀取或者更新的部分。數據綁定指令提供了你的Model投射到view的方法。這些投射能夠無縫的,絕不影響的應用到web應用中。
傳統來講,當Model變化了。 開發人員須要手動處理DOM元素而且將屬性反映到這些變化中。這個一個雙向的過程。一方面,model變化驅動了DOM中元素變化,另外一方面,DOM元素的變化也會影響到Model。這個在用戶互動中更加複雜,由於開發人員須要處理和解析這些互動,而後融合到一個model中,而且更新View。這是一個手動的複雜過程,當一個應用很是龐大的時候,將會是一件很是費勁的事情。
這裏確定有更好的解決方案!那就是AngularJS的雙向數據綁定,可以同步DOM和Model等等。
這裏有一個很是簡單的例子,用來演示一個input輸入框和<h1>元素的雙向綁定(例01):
<!-- 例01 -->
<!doctype html>
<html ng-app="demoApp">
<head>
<script src="./js/angular.min.js"></script>
</head>
<body>
<div>
<label>Name:</label>
<input type="text" ng-model="user.name" placeholder="請輸入名字">
<hr>
<h1>Hello, {{user.name}}!</h1>
</div>
</body>
</html>
在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="{{image.image}}" alt="{{image.description}}">
</li>
</ul>
</div>
這裏還有一件事值得提一句,AngularJS並不強制你學習一個新的語法或者從你的應用中提出你的模板。
針對客戶端應用開發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處理佈局和互動。
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'];
指令是我我的最喜歡的特性。你是否是也但願瀏覽器能夠作點兒有意思的事情?那麼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>
使用一系列的組件來建立你本身的應用將會讓你更方便的添加,刪除和更新功能。
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);" ng-click="getAjaxUser()">AJAX獲取名字</a>
</div> ……
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也會自動更新。
$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 }} 。第一部分中的例子就是靠這一點成功運行的。
首先須要明確一下模板的概念。在我還不知道有模板這個東西的時候,曾經用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', []);
要明確建立一個$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們,這個會在指令詳解中說明。
$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基本一致,這裏就很少說了。
ng中的表達式與javascript表達式相似可是不能夠劃等號,它是ng本身定義的一套模式。表達式能夠做爲指令的值,如ng-modle=」people.name」、ng-click=」showMe()」,看起來是如此像字符串,故而也叫字符串表達式。也能夠在標記中使用表達式,如{{1+2}},或者與過濾器一塊兒使用{{1+2 | currency}}。在框架內部,字符串不會簡單的使用eval()來執行,而是有一個專門的$parse服務來處理。在ng表達式中不能夠使用循環語句、判斷語句,事實上在模板中使用複雜的表達式也是一個不推薦的作法,這樣視圖與邏輯就混雜在一塊兒了。
咱們在使用其餘模板庫時,通常都會有模板的循環輸出、分支輸出、邏輯判斷等相似的控制。
要想理解指令屬性的運做,咱們必須先理解表達式。在以前的例子裏咱們已經見過表達式,例如 {{ user.name }}。請查看例0三、例0四、例05。
{{ 8 + 1 }} 9
{{ person }} {"name":"Ari Lerner"}
{{ 10 * 3.3 | currency }} $33.00
表達式粗略來看有點像 eval(javascript) 的結果。它們會通過Angular.js的處理,從而擁有如下重要而獨特的性質:
1.全部表達式都在scope這個context裏被執行,所以能夠使用全部本地 $scope 中的變量。
2.若是一個表達式的執行致使類型錯誤或引用錯誤,這些錯誤將不會被拋出。
3.表達式裏不容許任何控制函數流程的功能(如if/else等條件語句)
4.表達式可接受一個或多個串聯起來的過濾器。
過濾器(filter)正如其名,做用就是接收一個輸入,經過某個規則進行處理,而後返回處理後的結果。主要用在數據的格式化上,例如獲取一個數組中的子集,對數組中的元素進行排序等。過濾器一般是伴隨標記來使用的,將你model中的數據格式化爲須要的格式。表單的控制功能主要涉及到數據驗證以及表單控件的加強。ng內置了一些過濾器,它們是:
currency(貨幣)、date(日期)、filter(子串匹配)、json(格式化json對象)、limitTo(限制個數)、lowercase(小寫)、uppercase(大寫)、number(數字)、orderBy(排序)。
總共九種。除此以外還能夠自定義過濾器,這個就強大了,能夠知足任何要求的數據處理。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了。
ng內置了九種過濾器,使用方法都很是簡單,看文檔即懂。不過爲了之後不去翻它的文檔,我在這裏仍是作一個詳細的記錄。
currency(貨幣)、date(日期)、filter(子串匹配)、json(格式化json對象)、limitTo(限制個數)、lowercase(小寫)、uppercase(大寫)、number(數字)、orderBy(排序)
使用currency能夠將數字格式化爲貨幣,默認是美圓符號,你能夠本身傳入所需的符號,例如我傳入人民幣:
{{num | currency : '¥'}}
原生的js對日期的格式化能力有限,ng提供的date過濾器基本能夠知足通常的格式化要求。用法以下:
{{date | date : 'yyyy-MM-dd hh:mm:ss EEEE'}}
參數用來指定所要的格式,y M d h m s E 分別表示 年 月 日 時 分 秒 星期,你能夠自由組合它們。也能夠使用不一樣的個數來限制格式化的位數。另外參數也能夠使用特定的描述性字符串,例如「shortTime」將會把時間格式爲12:05 pm這樣的。ng提供了八種描述性的字符串,我的以爲這些有點多餘,我徹底能夠根據本身的意願組合出想要的格式,不肯意去記這麼多單詞~
這個名叫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的
json過濾器能夠把一個js對象格式化爲json字符串,沒有參數。這東西有什麼用呢,我通常也不會在頁面上輸出一個json串啊,官網說它能夠用來進行調試,嗯,是個不錯的選擇。或者,也能夠用在js中使用,做用就和咱們熟悉的JSON.stringify()同樣。用法超級簡單:
{{ jsonTest | json}}
limitTo過濾器用來截取數組或字符串,接收一個參數用來指定截取的長度,若是參數是負值,則從數組尾部開始截取。我的以爲這個filter有點雞肋,首先只能從數組或字符串的開頭/尾部進行截取,其次,js原生的函數就能夠代替它了,看看怎麼用吧:
{{ childrenArray | limitTo : 2 }} //將會顯示數組中的前兩項
把數據轉化爲所有小寫。太簡單了,很少解釋。一樣是很雞肋的一個filter,沒有參數,只能把整個字符串變爲小寫,不能指定字母。怎麼用我都懶得寫了。
同上。
number過濾器能夠爲一個數字加上千位分割,像這樣,123,456,789。同時接收一個參數,能夠指定float類型保留幾位小數:
{{ num | number : 2 }}
orderBy過濾器能夠將一個數組中的元素進行排序,接收一個參數來指定排序規則,參數能夠是一個字符串,表示以該屬性名稱進行排序。能夠是一個函數,定義排序屬性。還能夠是一個數組,表示依次按數組中的屬性值進行排序(若按第一項比較的值相等,再按第二項比較),仍是拿上面的孩子數組舉例:
<div>{{ childrenArray | orderBy : 'age' }}</div> //按age屬性值進行排序,如果-age,則倒序 <div>{{ childrenArray | orderBy : orderFunc }}</div> //按照函數的返回值進行排序 <div>{{ childrenArray | orderBy : ['age','name'] }}</div> //若是age相同,按照name進行排序
內置的過濾器介紹完了,正如你所看到的,ng內置的過濾器也並非萬能的,事實上好多都比較雞肋。更個性化的需求就須要咱們來定義本身的過濾器了,下面來看看如何自定義過濾器。
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 "";} }; });
經過使用模板,咱們能夠把model和controller中的數據組裝起來呈現給瀏覽器,還能夠經過數據綁定,實時更新視圖,讓咱們的頁面變成動態的。
模板中能夠使用的東西包括如下四種:
1.指令(directive):ng提供的或者自定義的標籤和屬性,用來加強HTML表現力;
2.標記(markup):即雙大括號{{}},可將數據單向綁定到HTML中;
3.過濾器(filter):用來格式化輸出數據;
4.表單控制:用來加強表單的驗證功能。
其中,指令無疑是使用量最大的,ng內置了不少指令用來控制模板,如ng-repeat,ng-class,也有不少指令來幫你完成業務邏輯,如ng-controller,ng-model。
指令的幾種使用方式以下:
1.做爲標籤:<my-dir></my-dir>
2.做爲屬性:<span my-dir="exp"></span>
3.做爲註釋:<!-- directive: my-dir exp -->
4.做爲類名:<span class="my-dir: exp;"></span>
其實經常使用的就是做爲標籤和屬性。
既然模板就是普通的HTML,那我首要關心的就是樣式的控制,元素的定位、字體、背景色等等如何能夠靈活控制。下面來看看經常使用的樣式控制指令。
1. ng-class
ng-class用來給元素綁定類名,其表達式的返回值能夠是如下三種:
1.類名字符串,能夠用空格分割多個類名,如’redtext boldtext’;
2.類名數組,數組中的每一項都會層疊起來生效;
3.一個名值對應的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來控制元素的顯隱。
對於經常使用的表單控件功能,ng也作了封裝,方便靈活控制。
ng-checked控制radio和checkbox的選中狀態
ng-selected控制下拉框的選中狀態
ng-disabled控制失效狀態
ng-multiple控制多選
ng-readonly控制只讀狀態
以上指令的取值均爲boolean類型,當值爲true時相關狀態生效,道理比較簡單就很少作解釋。注意: 上面的這些只是單向綁定,即只是從數據到模板,不能副作用於數據。要雙向綁定,仍是要使用 ng-model 。
事件綁定是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,讓它有一點業務能力。這麼想的話彷佛也能想通,好吧,先欺騙一下本身吧~
在說明這兩個指令的特殊以前,須要先了解一下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=」{{imgUrl}}」 />
那麼在頁面開始加載到ng編譯完成以前,頁面上會一直顯示一張錯誤的圖片,由於路徑{{imgUrl}}還未被替換。
爲了不這種狀況,咱們使用ng-src指令,這樣在路徑被正確獲得以前就不會顯示找不到圖片。同理,<a>標籤的href屬性也須要換成ng-href,這樣頁面上就不會先出現一個地址錯誤的連接。
順着這個思路再多想一點,咱們在模板中使用{{}}顯示數據時,在ng編譯完成以前頁面上豈不是會顯示出大括號及裏面的表達式?確實是這樣。爲了不這個,ng中有一個與{{}}等同的指令:ng-bind,一樣用於單向綁定,在頁面剛加載的時候就不會顯示出對用戶無用的數據了。儘管這樣你可能不但沒舒心反而更糾結了,{{}}那麼好用易理解,還不能用了不成?好消息是咱們依然能夠使用。由於我編寫的是單頁面應用,頁面只會在加載index.html的時
候出這個問題,只需在index.html中的模板中換成ng-bind就行。其餘的模板是咱們動態加載的,就能夠放心使用{{}}了。
下面咱們來解析下指令的例子(例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。
服務這個概念其實並不陌生,在其餘語言中如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、指令或者是其餘服務中。
如同指令同樣,系統內置的服務以$開頭,咱們也能夠本身定義一個服務。定義服務的方式有以下幾種:
1.使用系統內置的$provide服務;
2.使用Module的factory方法;
3.使用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來訪問數據了。
服務與服務中間能夠有依賴關係,例如咱們這裏定義一個名爲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); } }]);
接下來讓咱們看下例子(例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);" ng-click="getUserInfo(article_.userId)"> {{article_.userName}} </a> </td> </tr> ...... <div ng-show="showUserInfo"> 用戶ID:{{currentUser.userId}}<br/> 用戶名:{{currentUser.userName}} <br/> 用戶簡介:{{currentUser.userInfo}} <br/> </div>
經過依賴注入,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);" ng-repeat="t in templates">{{t}} </a><br />
在談路由機制前有必要先提一下如今比較流行的單頁面應用,就是所謂的single page APP。爲了實現無刷新的視圖切換,咱們一般會用ajax請求從後臺取數據,而後套上HTML模板渲染在頁面上,然而ajax的一個致命缺點就是致使瀏覽器後退按鈕失效,儘管咱們能夠在頁面上放一個大大的返回按鈕,讓用戶點擊返回來導航,但老是沒法避免用戶習慣性的點後退。解決此問題的一個方法是使用hash,監聽hashchange事件來進行視圖切換,另外一個方法是用HTML5的history API,經過pushState()記錄操做歷史,監聽popstate事件來進行視圖切換,也有人把這叫pjax技術。
如此一來,便造成了經過地址欄進行導航的深度連接(deeplinking ),也就是咱們所須要的路由機制。經過路由機制,一個單頁應用的各個視圖就能夠很好的組織起來了。
ng的路由機制是靠ngRoute提供的,經過hash和history兩種方式實現了路由,能夠檢測瀏覽器是否支持history來靈活調用相應的方式。ng的路由(ngRoute)是一個單獨的模塊,包含如下內容:
1.服務$routeProvider用來定義一個路由表,即地址欄與視圖模板的映射
2.服務$routeParams保存了地址欄中的參數,例如{id : 1, name : 'tom'}
3.服務$route完成路由匹配,而且提供路由相關的屬性訪問及事件,如訪問當前路由對應的controller
4.指令ngView用來在主視圖中指定加載子視圖的區域
以上內容再加上$location服務,咱們就能夠實現一個單頁面應用了。下面來看一下具體如何使用這些內容。
第一步:引入文件和依賴
ngRoute模塊包含在一個單獨的文件中,因此第一步須要在頁面上引入這個文件,以下:
<script src="http://code.angularjs.org/1.2.8/angular.min.js"></script> <script src="http://code.angularjs.org/1.2.8/angular-route.min.js"></script>
光引入還不夠,咱們還需在模塊聲明中注入對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匹配後所需的一系列配置項,包括如下內容:
1) controller //function或string類型。在當前模板上執行的controller函數,生成新的scope; 2) controllerAs //string類型,爲controller指定別名; 3) template //string或function類型,視圖z所用的模板,這部份內容將被ngView引用; 4) templateUrl //string或function類型,當視圖模板爲單獨的html文件或是使用了<script type="text/ng-template">定義模板時使用; 5) resolve //指定當前controller所依賴的其餘模塊; 6) 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>
咱們的子視圖將會在此處被引入進來。完成這三步後,你的程序的路由就配置好了。
下面咱們將用一個例子(例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="#/list/{{ user.userId }}"> {{ user.userName }}</a> </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="#/list">返回</a> </div>
4. 路由局部刷新位置:
<h1>AngularJS路由(Route) 示例</h1> <div ng-view></div>
NG動畫效果,如今能夠經過CSS3或者是JS來實現,若是是經過JS來實現的話,須要其餘JS庫(好比JQuery)來支持,實際上底層實現仍是靠其餘JS庫,只是NG將其封裝了,使其更易使用。
NG動畫效果包含如下幾種:
其相關參數爲:
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) {} }; });
下面咱們來看下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);" ng-click="testShow=!testShow" >Change</a>
ng-show爲false時會爲其加上「ng-hide「的CLASS; ng-show爲true時會爲其移除「ng-hide「的CLASS,從而觸發動畫效果。
3.其餘代碼:
demoApp.controller("test10Controller", function($scope, $animate) { $scope.testShow = true; });
略
Angular用戶都想知道數據綁定是怎麼實現的。你可能會看到各類各樣的詞彙:$watch、$apply、$digest、dirty-checking...它們是什麼?它們是如何工做的呢?這裏我想回答這些問題,其實它們在官方的文檔裏都已經回答了,可是我仍是想把它們結合在一塊兒來說,可是我只是用一種簡單的方法來說解,若是要想了解技術細節,查看源代碼。
咱們的瀏覽器一直在等待事件,好比用戶交互。假如你點擊一個按鈕或者在輸入框裏輸入東西,事件的回調函數就會在javascript解釋器裏執行,而後你就能夠作任何DOM操做,等回調函數執行完畢時,瀏覽器就會相應地對DOM作出變化。(記住,這是個重要的概念),爲了解釋什麼是context以及它如何工做,咱們還須要解釋更多的概念。
每次你綁定一些東西到你的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。
還記得我前面提到的擴展的事件循環嗎?當瀏覽器接收到能夠被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。
誰決定什麼事件進入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不是什麼魔術師,他也不知道第三方庫想要更新綁定的數據。
你已經知道了咱們設置的任何綁定都有一個它本身的$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也會改變,因此可以正確觸發。
我但願大家已經學會了在Angular中數據綁定是如何工做的。我猜測你的第一印象是dirty-checking很慢,好吧,實際上是不對的。它像閃電般快。可是,若是你在一個模版裏有2000-3000個watch,它會開始變慢。可是我以爲若是你達到這個數量級,就能夠找個用戶體驗專家諮詢一下了。
不管如何,隨着ECMAScript6的到來,在Angular將來的版本里咱們將會有Object.observe那樣會極大改善$digest循環的速度。
angular的指令機制。angular經過指令的方式實現了HTML的擴展,加強後的HTML不只長相面目一新,同時也得到了不少強大的技能。更厲害的是,你還能夠自定義指令,這就意味着HTML標籤的範圍能夠擴展到無窮大。angular賦予了你造物主的能力。既然是做爲angular的精華之一,相應的指令相關的知識也不少的。
在開始自定義指令以前,咱們有必要了解一下指令在框架中的執行流程:
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編譯的工做是解析指令、綁定監聽器、替換模板中的變量等。由於工做方式很像高級語言編輯中的遞歸、堆棧過程,因此起名爲編譯,不要疑惑。
指令的幾種使用方式以下:
其實經常使用的就是做爲標籤和屬性,下面兩種用法目前還沒見過,感受就是用來賣萌的,姑且留個印象。咱們自定義的指令就是要支持這樣的用法。
關於自定義指令的命名,你能夠隨便怎麼起名字都行,官方是推薦用[命名空間-指令名稱]這樣的方式,像ng-controller。不過你可千萬不要用ng-前綴了,防止與系統自帶的指令重名。另一個需知道的地方,指令命名時用駝峯規則,使用時用-分割各單詞。如:定義myDirective,使用時像這樣:<my-directive>。
下面是定義一個標準指令的示例,可配置的參數包括如下部分:
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。
指令的表現配置參數: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.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函數中。
咱們在上面寫了一個簡單的<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的定義中大致是這個樣子:
app.directive('sayHello', function() { return { restrict: 'E', template: '<div>hello,<b ng-transclude></b>!</div>', replace: true, transclude: true }; })
所謂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參數使用了=號來指定獲取屬性的類型爲父做用域的屬性,若是咱們想在指令中使用父做用域中的函數,使用&符號便可,是一樣的原理。
使用指令來定義一個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: ' 個人性格就是無性格' } ];
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」過濾器。能夠經過限制顯示列表的大小來減小渲染時間。這是減小渲染時間最高效的方法。
1.渲染沒有數據綁定的列表
這是最明顯的解決方案,由於數據綁定是性能問題最可能的根源。若是你只想顯示一次列表,並不須要更新、改變數據,放棄數據綁定是絕佳的辦法。不過惋惜的是,你會失去對數據的控制權,但除了該法,咱們別無選擇。
2.不要使用內聯方法計算數據
爲了在控制器中直接過濾列表,不要使用可得到過濾連接的方法。「ng-repeat」會評估每一個表達式。在咱們的案例中,「filteredItems()」返回過濾連接。若是評估過程很慢,它將迅速下降整個應用的速度。
<li ng-repeat="item in filteredItems()"> //這並非一個好方法,由於要頻繁地評估。 <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=i tem in items | filter:filter.query>{{ item.title }} </li>
angular上手比較難,初學者(特別是習慣了使用JQuery的人)可能不太適應其語法以及思想。隨着對ng探索的一步步深刻,也確實感受到了這一點,尤爲是框架內部的某些執行機制。
ng-show ng-hide 無動畫效果問題
<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很大,就會綁定太多的事件,性能出現問題。
從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 上添加代理事件。