本文引自
http://blog.csdn.net/kongjiea/article/details/49840035
指令,很重要
AngularJS與jQuery最大的區別在哪里?我認為,表現在數據雙向綁定,實質就是DOM的操作形式不一樣。
-
JQuery通過選擇器找到DOM元素,再賦予元素的行為;
-
而AngularJS則是,將指令與DOM綁定在一起,再擴展指令的行為。
所以AngularJS開發最理想的結果就是,在頁面HTML與CSS的設計時,設計工程師只需要關注指令的使用;而在背后的邏輯開發上,架構工程師則是不需要知道如何操作DOM,只需要關注指令背后的行為要如何實現就行;測試工程師也可以開發針對指令的單元測試。
指令就是DOM與邏輯行為的媒介,本質就是DOM綁定的獨立邏輯行為函數。
指令難點在於參數
來看看都有哪些
angular.module('app', []) .directive('myDirective', function() { return { restrict: String, priority: Number, terminal: Boolean, template: String or Template Function: function(tElement, tAttrs) {...}, templateUrl: String, replace: Boolean or String, scope: Boolean or Object, transclude: Boolean, controller: String or function(scope, element, attrs, transclude, otherInjectables) { ... }, controllerAs: String, require: String, link: function(scope, iElement, iAttrs) { ... }, compile: // 返回一個對象或連接函數,如下所示: function(tElement, tAttrs, transclude) { return { pre: function(scope, iElement, iAttrs, controller) { ... }, post: function(scope, iElement, iAttrs, controller) { ... } } return function postLink(...) { ... } } }; });
剛開始接觸指令的時候,我簡直就是蒙了,這堆參數究竟怎么用怎么理解啊。告訴大家我的一個理解方法。
把它們分成三類:
-
描述指令或DOM本身特性的內部參數
-
連接指令外界、與其他指令或控制器溝通的對外參數
-
描述指令本身行為的行為參數
內部參數
-
restrict:String,E(元素)
<my-directive></my-directive>
A(屬性,默認值)<div my-directive="expression"></div>
C(類名)<div class="my-directive:expression;"></div>
M(注釋)<--directive:my-directive expression-->
-
priority: Number,指令執行優先級
-
template: String,指令鏈接DOM模板,例如“<h1>{{head}}</h1>”
-
templateUrl:String,DOM模板路徑
-
replace: Boolean,指令鏈接模板是否替換原有元素,
對外參數——scope
scope參數非常重要,本應該是放到最后說明的,但是scope卻是理解其他參數的關鍵,所以務必先跟大家說清楚。
scope參數的作用是,隔離指令與所在控制器間的作用域、隔離指令與指令間的作用域。
scope參數是可選的,默認值為false,可選true、對象{};
-
false:共享父域
-
true:繼承父域,且新建獨立作用域
-
對象{}:不繼承父域,且新建獨立作用域
false、true、{}三者對比
來看個例子
<body> <div ng-controller='parentCtrl'> <h3>指令scope參數——false、true、{}對比測試</h3> parent: <div> <span> {{parentName}}</span> <input type="text" ng-model="parentName" /> </div> <br /> <child-a></child-a> <br /> <child-b></child-b> <br /> <child-c parent-name="parentName"></child-c> </div> <!--t1指令模板--> <script type="text/html" id="t1"> <div> <span>{{parentName}}</span> <input type="text" ng-model="parentName" /> </div> </script> <script> var app = angular.module("app", []); app.controller('parentCtrl', function ($scope) { $scope.parentName = "parent"; }) //false:共享作用域 app.directive('childA', function () { return { restrict: 'E', scope: false, template: function (elem, attr) { return "false:" + document.getElementById('t1').innerHTML; } }; }); //true:繼承父域,並建立獨立作用域 app.directive('childB', function () { return { restrict: 'E', scope: true, template: function (elem, attr) { return "true:" + document.getElementById('t1').innerHTML; }, controller: function ($scope) { $scope.parentName = "parent"; //已聲明的情況下,$scope.$watch監聽的是自己的parentName $scope.$watch('parentName', function (n, o) { console.log("child watch" + n); }); //$scope.$parent.$watch監聽的是父域的parentName $scope.$parent.$watch('parentName', function (n, o) { console.log("parent watch" + n); }); } }; }); //{}:不繼承父域,建立獨立作用域 app.directive('childC', function () { return { restrict: 'E', scope: {}, template: function (elem, attr) { return "{}:" + document.getElementById('t1').innerHTML; }, controller: function ($scope) { console.log($scope); } }; }); </script> </body>
false參數
本質:子域與父域共享作用域。
特點:父域修改parentName的同時,指令綁定的parentName的元素會被刷新。
反之,指令內部parentName被修改時,父域的parentName同樣會被刷新。
true參數
本質:子域繼承父域,並建立獨立作用域。
特點:
1、在指令已聲明parentName的情況下,父域parentName變更,指令中parentName不會發生變化。
指令在true參數下,建立了的scope,獨立並隔離與父控制器的scope。
controller: function ($scope) { $scope.parentName = "parent"; }
反之,指令中parentName變更,父域也不會發生變化。
2、在指令未聲明parentName的情況下,父域的parentName變更,指令中parentName也會刷新
這種情況很多時候會被忽略,指令的scope沒有聲明對象時,其元素綁定的仍然是父域的對象。但,一旦指令中Input變更了,對應的獨立scope也會自動聲明該綁定對象,這就回到了第1種情況。
controller: function ($scope) { //$scope.parentName = "parent"; }
然而,指令中parentName變更,父域是不會變化的;
3、在指令已聲明parentName的情況下 ,在指令中監聽父域parentName 的變化無效。但監聽子域parentName的變化有效
獨立子域scope,只能監聽自己的,不能監聽父域的。但通過 $scope.$parent可以監聽父域。
controller: function ($scope) { $scope.parentName = "parent" ; //已聲明的情況下,$scope.$watch監聽的是自己的parentName $scope.$watch( 'parentName' , function (n, o) { console.log("child watch" + n); }); //$scope.$parent.$watch監聽的是父域的parentName $scope.$parent.$watch( 'parentName' , function (n, o) { console.log("parent watch" + n); }); }
4、在指令未聲明parentName的情況下 ,在指令中監聽父域parentName的變化有效。
這里就不解釋了,參考第2點,大家可以動手試一下。
controller: function ($scope) { //$scope.parentName = "parent"; //未聲明的情況下,$scope.$watch監聽的是父域的parentName $scope.$watch('parentName' , function (n, o) { console.log("child watch" + n); }); }
對象{}參數
本質:子域不繼承父域,並建立獨立作用域。
簡單來說:
@: 單向數據綁定:從綁定根元素到模板指定元素:用來傳遞普通字符串
=: 雙向數據綁定, 用於傳遞對象
&: 傳遞函數:
指令元素: <nel-scope modelone="{{one}}" modeltwo="two" methd="hiNiko()"></nel-scope> 指令js: app.directive('nelScope', function() { return { restrict: 'AE', replace: false, templateUrl: 'template1.html', scope: { model1: '@modelone', model2: '=modeltwo', popup: '&methd' } } }); 模板: <div class="col-md-6"> <input type="text" ng-model="model1" /><br /> <input type="text" ng-model="model2"/> <input type="button" value="hi" ng-click="popup()" /> </div>
特點:
1、當scope對象為空對象時,無論是父域parentName,還是指令子域parentName發生變更,都不會影響到對方。
原理很清楚,就是指令建立的獨立作用域,與父域是完全隔離的。
scope: {}
2、當scope對象為非空對象時,指令會將該對象處理成子域scope的擴展屬性。而父域與子域之間傳遞數據的任務,就是可以通過這塊擴展屬性完成。
<div ng-controller='parentCtrl'> parent: <p><span>{{name}}</span><input type="text" ng-model="name" /></p> <p><span>{{sexy}}</span><input type="text" ng-model="sexy" /></p> <p><span>{{age}}</span><input type="text" ng-model="age" /></p> <br /> <!--特別注意:@與=對應的attr,@是單向綁定父域的機制,記得加上{{}};&對應的attrName必須以on-開頭--> <child-c my-name="name" my-sexy-attr="sexy" my-age="{{age}}" on-say="say('i m ' + name)"></child-c> </div> <!--t1指令模板--> <script type="text/html" id="t1"> <div> <span>{{myName}}</span> <input type="text" ng-model="myName" /> </div> <div> <span>{{mySexy}}</span> <input type="text" ng-model="mySexy" /> </div> <div> <span>{{myAge}}</span> <input type="text" ng-model="myAge" /> </div> </script> <script> var app = angular.module("app", []); app.controller('parentCtrl', function ($scope) { $scope.name = "mark"; $scope.sexy = "male"; $scope.age = "30"; $scope.say = function (sth) { alert(sth); }; }) app.directive('childC', function () { return { restrict: 'E', scope: { myName: '=', mySexy: '=mySexyAttr', myAge: '@', onSay: '&' }, template: function (elem, attr) { return "{}:" + document.getElementById('t1').innerHTML; }, controller: function ($scope) { console.log($scope.myName); console.log($scope.mySexy); console.log($scope.myAge); $scope.onSay(); } }; }); </script>
@(or @Attr)綁定策略——本地作用域屬性,使用@符號將本地作用域同DOM屬性的值進行綁定。指令內部作用域可以使用外部作用域的變量。(單向引用父域對象)
<child-c my-age="{{age}}"></child-c>
ps:@ 是單向綁定本地作用域,記得加上{{}}
scope: { myAge: '@', }
= (or =Attr)綁定策略——雙向綁定:通過=可以將本地作用域上的屬性同父級作用域上的屬性進行雙向的數據綁定。就像普通的數據綁定一樣,本地屬性會反映出父數據模型中所發生的改變。(雙向引用父域對象)
<child-c onSay="name"></child-c>
ps:=策略不需要加上{{}}進行綁定
scope: { myName: '=', }
& (or &Attr)綁定策略——通過&符號可以對父級作用域進行綁定,以便在其中運行函數。(調用父域函數)
<child-c on-say="say('i m ' + name)"></child-c>
ps:&對應的attrName必須以on-開頭
scope: { onSay: '&', }
父域綁定調用函數及傳參
app.controller('parentCtrl', function ($scope) { $scope.say = function (sth) { alert(sth); }; })
ps特別注意:@與=對應的attr,;
<child-c my-name="name" my-sexy-attr="sexy" my-age="{{age}}" on-say="say('i m ' + name)"></child-c>
總結下來,scope擴展對象,既能夠解耦父域與子域共域的問題,也能夠實現指令與外界通訊的問題,是Angular開發指令化模塊化的重要基礎。在往后的章節,我會向大家介紹指令化開發的更多實例。
對外參數——require
scope是指令與外界作用域通訊的橋梁,而require是指令與指令之間通訊的橋梁。這個參數最大的作用在於,當要開發單指令無法完成,需要一些組合型指令的控件或功能,例如日期控件,通過require參數,指令可以獲得外部其他指令的控制器,從而達到交換數據、事件分發的目的。
使用方法:require: String or Array
——String值為引入指令名稱,並且有兩個尋找指令策略符號‘?’與‘^’;Array數組則為多個外部指令名稱。
在link函數第4個參數ctrl中獲取注入外部指令的控制器,如果require為String,ctrl為對象,如果require是數組,ctrl為數組。
require: '^teacher1', link: function ($scope, $element, $attrs, ctrl) { //ctrl指向teacher1指令的控制器 }
?策略——尋找指令名稱,如果沒有找到,link函數第4個參數為null;如果沒有?,則報錯。
^ 策略——在自身指令尋找指令名稱的同時,向上父元素尋找;如果沒有^,則僅在自身尋找。
如下例子,指令studentA
向上可以找到指令teacher
及自身,但是不能找到相鄰兄弟的student-b
。
<div teacher> <student-a></student-a> <student-b></student-b> </div>
完整例子
<body> <div teacher> {{name}} <student-a></student-a> <student-b></student-b> </div> <script> var app = angular.module("app", []); //studentA——require指向父級指令teacher app.directive('studentA', function () { return { require: '?^teacher', scope: {}, template: '<div>A`s teacher name: <span>{{teacherName}}</span></div>', link: function ($scope, $element, $attrs, ctrl) { //獲取teacher指令控制器,並調用其方法sayName() $scope.teacherName = ctrl.sayName(); } }; }); //studentB——require指向父級指令teacher,及指令studentA //但是,由於不能獲得兄弟,也沒有采取?策略,導致報錯 app.directive('studentB', function () { return { require: ['?^teacher', 'studentA'], scope: {}, template: '<div>B`s teacher name: <span>{{teacherName}}</span></div>', link: function ($scope, $element, $attrs, ctrl) { $scope.teacherName = ctrl.sayName(); } }; }); app.directive('teacher', function () { return { restrict: 'A', controller: function ($scope) { $scope.name = "Miss wang"; //擴展控制器的方法sayName,目的是讓外部內獲取控制器內部數據 this.sayName = function () { return $scope.name; }; } }; }); </script> </body>
既然require可以獲取外部指令,那Angular原生指令應該也是能夠獲取。其中最廣泛應用的就是require: 'ngModel'
,關於ngModel在自定義指令上的應用,留待下回實例中再跟大家深入討論。
行為參數——link與controller
為什么要把link與controller兩個參數放到一起?
因為很多童鞋會把它們錯誤地混淆使用,包括我自己。
link與controller都是描述指令行為的參數,但它們是要描述的行為是完全不同的類型。
controller語法
controller:String or Function
controller本身的意義就是賦予指令控制器,而控制器就是定義其內部作用域的行為的。
所以controller要描述的是:指令的作用域的行為。
//指向匿名控制器 controller: function ($scope) { }, //指向控制器mainCtrl controller: "mainCtrl"
link語法
link:String Or Function
link名稱是鏈接函數,啥意思,好像挺難理解。所以在解釋鏈接函數之前,先要說一下Angular的初始化對於指令究竟做了什么。
Angular在剛從HTTP Response接收靜態素材之初,會首先去分析母頁HTML中有哪些原生指令或自定義指令,然后再去加載指令的template模板HTML,而template模板中又去加載自己的指令模板,如此類推,直到Angular找到了所有的指令及模板,形成模板樹,並返回模板函數,提供給下一階段進行數據綁定。
<body> <stu1 ></ stu1> <script > var app = angular.module("app" , []); app.directive( 'stu1' , function () { return { restrict: 'E' , template: "<p>1</p><stu2></stu2>" , link: function (scope) { console.log( 'stu1 running' ); } }; }); app.directive( 'stu2' , function () { return { restrict: 'E' , template: "<p>2</p><stu3></stu3>" , link: function (scope) { console.log( 'stu2 running' ); } }; }); app.directive( 'stu3' , function () { return { restrict: 'E' , template: "<p>3</p>" , link: function (scope) { console.log( 'stu3 running' ); } }; }); </script > </ body> console output stu3 running stu2 running stu1 running
注意以上例子,在第一個斷點stu3 running
的時候,1 2 3 三個模板都渲染完成了。然后從最根部的stu3的link函數開始,依次執行stu 3 stu2 stu1的link函數。
簡單來說就是:
-
加載模板,形成DOM模板樹
-
@@@@
-
數據綁定
@@@@是啥?沒錯,就是link鏈接函數,它會在形成模板樹之后,在數據綁定之前,從最底部指令開始,逐個指令執行它們的link函數。
在這個時間節點的link函數,操作DOM的性能開銷是最低,非常適合在這個時機執行DOM的操作,例如鼠標操作或觸控事件分發綁定、樣式Class設置、增刪改元素等等。
所以link就是描述指令元素操作行為。
link: function (scope, element, attr, ctrl) { element.bind("click", function () { console.log("綁定點擊事件"); }); element.append("<p>增加段落塊</p>"); //設置樣式 element.css("background-color", "yellow"); //不推薦,在link中賦予scope行為 scope.hello = function () { console.log("hello"); }; }
同理,在link中定義$scope行為是不推薦的。
這樣想想,對於controller與link,就明白了。但還有一個問題,它們倆的執行順序是?答案是先controller,后link。
放到全局順序就是:
-
執行controller,設置各個作用域scope
-
加載模板,形成DOM模板樹
-
執行link,設置DOM各個行為
-
數據綁定,最后scope綁上DOM
例子
<div student> {{name }} </div> <script> var app = angular.module("app", []); app.directive('student', function () { return { restrict: 'A', controller: function ($scope) { $scope.name = "tgor"; console.log('controller running'); }, link: function (scope, el) { el.append("<p>hello</p>"); console.log('link running'); } }; }); </script>
自己添加:
指令之間通過require交互來通信
angular的指令間交互是通過別的指令來完成的, 一般都是借助於一個父指令來提供暴露在外的方法或者參數,以供子指令調用
例子:
// 父指令 emmsMobile.directive('tabCtrl', function() { return { restrict: 'C', controller: function() { this.menuToggle = true; this.someMethod = function() { alert('method'); } } } }); // 子指令 emmsMobile.directive('tabCtrlItem', function() { return { restrict: 'C', require: '^tabCtrl', //通過require父指令來獲取父指令的controller引用,並將其引用作為link函數的第四個參數 link: function(scope, element, attrs, tabCtrl) { element.bind('click', function() { if (tabCtrl.menuToggle) { element.css('color', '#007aff'); element.css('border-bottom', '1px solid #007aff'); } }); element.bind('blur', function() { element.css('color', 'red'); element.css('border-bottom', '1px solid #007aff'); }); } } });