AngularJS 指令学习笔记
AngularJS怎样处理指令其实是依赖于指令定义时返回的对象属性的,所以要想深入理解如何定义一个指令,首相需要理解指令定义时各个参数的含义。
完整的AngularJS指令参数
angular.module('app',[])
.directive('demoDirective',function (){ // 依据官方规范,指令的定义时应该严格遵循驼峰式命名规则,使用时采用'-'连接单词
return {
restrict : String in ['E','A','C','M'],priority : Number,terminal : Boolean,template : String or Template Function : function (tElement,tAttrs) {...},templateUrl : String,require : String or String Array,replace : Boolean or String,scope : Boolean or Object,transclude : false or element,controller : String or function (scope,element,attrs,transclude,otherInjectable) {...},controllerAs : String,link : function (scope,otherController) {...},compile : function (element,transclude) {
return object || function (...) {...}
}
};
});
上面的这么多参数如果按照功能划分的话,大致上可以分为如下三类:
1. 描述指令或是DOM自身特性的内部参数
2. 连接指令外界的参数,该类参数使得指令可与其他指令或控制器沟通
3. 描述指令自身行为的参数
内部参数
·restrict :’E’ – Element,‘A’ – Attribute,’C’ – Class,‘M’ – comMent
·priority :指令执行的优先级,默认值为0
·template : 与指令关联的HTML模板,
·**templateUrl :与指令关联的HTML模板路径,
·replace :是否采用HTML模板替换原有的元素
对外参数
scope
scope 参数的作用是:隔离指令与所在控制器(父级作用域的控制器)间的作用域,隔离指令与指令之间的作用域。
scope的值是可选的,可选值分别为:false,true,object,默认情况下为false;
·false 共享父作用域
·true 继承父作用域,并且新建独立作用域
·object 不继承父作用域,创建新的独立作用域
关于scope可选值的不同结果对比:
<!DOCTYPE html>
<html lang="zh_CN" ng-app="app">
<head>
<Meta charset="UTF-8">
<title>AngularJS Index</title>
<script type="text/javascript" src="lib/jquery-1.12.3.js"></script>
<script type="text/javascript" src="lib/angular.min.js"></script>
<script type="text/javascript" src="js/app.js"></script>
<style> .Box { max-width: 680px; min-height: 80px; margin: 10px; padding: 10px 15px; border: solid 1px #2a3ca2; } .Box span,.Box input { display: block; } input[type="text"] { width: 280px; margin: 5px 0; } </style>
</head>
<body>
<div ng-controller="parentCtrl">
<h3>scope参数选择不同值时的作用域情况对比</h3>
<span>parent</span>
<div class="Box" >
<span>{{parentName}}</span>
<input type="text" ng-model="parentName">
<span>{{scopeName}}</span>
<input type="text" ng-model="scopeName">
</div>
<span>scop=False</span>
<dir-f class="Box"></dir-f>
<span>scope=True</span>
<dir-t class="Box"></dir-t>
<span>scope={}</span>
<dir-o-n class="Box"></dir-o-n>
<span>scope={attrs}</span>
<dir-o class="Box"></dir-o>
</div>
</body>
</html>
app.js
;(function($) {
'use strict';
angular.module('app',[])
.controller('parentCtrl',['$scope',function($scope) {
$scope.parentName = 'parentName';
$scope.scopeName = 'scopeNameInParentScope';
}])
.directive('dirF',[function() {
// Runs during compile
return {
// name: '',
// priority: 1,
// terminal: true,
// scope: {},// {} = isolate,true = child,false/undefined = no change
// controller: function($scope,$element,$attrs,$transclude) {},
// require: 'ngModel',// Array = multiple requires,? = optional,^ = check parent elements
// restrict: 'A',// E = Element,A = Attribute,C = Class,M = Comment
// template: '',
// templateUrl: '',
// replace: true,
// transclude: true,
// compile: function(tElement,tAttrs,function transclude(function(scope,cloneLinkingFn){ return function linking(scope,elm,attrs){}})),
//link: function($scope,iElm,iAttrs,controller) {}
restrict: 'E',scope: false,replace: true,template: [
'<div>',' <span>parentName</span>',' <span>{{ parentName }}</span>',' <input type="text" ng-model="parentName" />',' <span>scopeName</span>',' <span>{{ scopeName }}</span>',' <input type="text" ng-model="scopeName" />','</div>'
].join(''),controller : ['$scope',function ($scope){
$scope.scopeName = '123456';
}]
};
}])
.directive('dirT',[function() {
return {
restrict: 'E',scope: true,controller: ['$scope',function ($scope) {
$scope.parentName = 'parentNameInDirectiveTrue';
//$scope.scopeName = 'scopeNameInDirectiveTrue';
// 在当前$scope中以已经生明的情况下,$scope.$watch监听的是自身的parentName
$scope.$watch('parentName',function(newValue,oblValue) {
console.log('directive-true -->> from watch scope parentName = ' + newValue);
});
// 监听父作用域中的parentName
$scope.$parent.$watch('parentName',oldValue) {
console.log('directive-true -->> from watch parent scope parentName = ' + newValue);
});
}]
};
}])
.directive('dirON',scope: {
},function ($scope) {
$scope.parentName = 'parentNameInDirectiveObjectWithNull';
//$scope.scopeName = 'scopeNameInDirectiveObjectWithNull';
// 在当前$scope中以已经生明的情况下,$scope.$watch监听的是自身的parentName
$scope.$watch('parentName',oblValue) {
console.log('directive-Object-Null -->> from watch scope parentName = ' + newValue);
});
// 监听父作用域中的parentName
$scope.$parent.$watch('parentName',oldValue) {
console.log('directive-Object-Null -->> from watch parent scope parentName = ' + newValue);
});
}]
}
}])
.directive('dirO',scope: {
parentName : '@'
},function ($scope) {
$scope.parentName = 'parentNameInDirectiveObject';
//$scope.scopeName = 'scopeNameInDirectiveObject';
// 在当前$scope中以已经生明的情况下,$scope.$watch监听的是自身的parentName
$scope.$watch('parentName',oblValue) {
console.log('directive-Object -->> from watch scope parentName = ' + newValue);
});
// 监听父作用域中的parentName
$scope.$parent.$watch('parentName',oldValue) {
console.log('directive-Object -->> from watch parent scope parentName = ' + newValue);
});
// 监听父作用域中的scopeName
$scope.$parent.$watch('scopeName',oldValue) {
console.log('directive-Object -->> from watch parent scope scopeName = ' + newValue);
});
}]
}
}]);
}(jQuery));
实验结果:
·scope取值为false时,指令和父作用域共同使用用同一个作用域–示例程序中的scopName
·scope取值为true时,指令和父作用域关系采用JavaScript的原型继承方式实现
·scope取值为object时,指令将采用完全独立的作用域,通过
scope取值为非空对象时属性扩展特性
自定义指令中,当scope取值为非空对象时,指令会将该对象处理成子域scope的扩展属性。这一扩展属性肩负起了指令和父作用域通信的任务。
scoep取值为对象时,对象的属性有三种不同的绑定策略,分别是:
‘@’ or ‘@alias’ | ‘=’ or ‘=alias’ | ‘&’
关于三者间的区别直接上代码,注意代码中的注释部分,解释了三者的区别:
html
<div ng-controller="parentScopeCtrl">
<div class="Box">
<p>parentScope :</p>
<span>id</span>
<span>{{ id }}</span>
<input type="text" ng-model="id">
<span>gender</span>
<span>{{ gender }}</span>
<input type="text" ng-model="gender">
<span>demo</span>
<span>{{ demo }}</span>
<input type="text" ng-model="demo">
<span>other</span>
<span>{{ other }}</span>
<input type="text" ng-model="other">
</div>
<!-- '@'和'='对应Attribute属性的值,'@'是单向绑定父域的机制,需要使用{{}}表达式;'&'对应的属性名必须要以on开头 -->
<dir-scope my-id="id" gender="gender" my-age="{{ age }}" on-speak="speak('demo')"></dir-scope>
app.js
.controller('parentScopeCtrl',function ($scope){
$scope.id = 12345;
$scope.gender = "male";
$scope.age = 24;
$scope.demo = "haha";
$scope.other = "Other";
$scope.speak = function (msg){
console.log(msg);
}
}])
.directive('dirScope',[function(){
return {
restrict : 'EA',scope : {
myAge : '@',/* 子作用能感知到父作用域的变更,反之不行; 需要注意的是,属性赋值时需要使用{{}}表达式,而且该属性的值类型永远是String,也即是执行{{}}表达式返回的String */
myId : '=',/* 父子作用域双向绑定 */
myGender : '=gender',/* 父子作用域双向绑定; 与'='不同在于使用属性时的采用gender='' */
onSpeak : '&' // 通常&
},template : [
'<div>',' <span>myID</span>',' <span>{{ myId }}</span>',' <input type="text" ng-model="myId" />',' <span>myGender</span>',' <span>{{ myGender }}</span>',' <input type="text" ng-model="myGender" />',' <span>myAge</span>',' <span>{{ myAge }}</span>',' <input type="text" ng-model="myAge" />',' <span>demo</span>',' <span>{{ demo }}</span>',' <input type="text" ng-model="demo" />',' <span>other</span>',' <span>{{ other }}</span>',' <input type="text" ng-model="other" />',' <button ng-click="log()">Show Different</button>',function ($scope){
$scope.log = function (){
console.log(typeof $scope.myId,$scope.myId);
console.log(typeof $scope.myGender,$scope.myGender);
console.log(typeof $scope.myAge,$scope.myAge);
console.log(typeof $scope.onSpeak,$scope.onSpeak);
$scope.onSpeak();
}
$scope.demo = "scope-hahaha"; // 完全与父域中同名属性隔离
// 指令中使用other属性,但是由于scope取值为对象,所以也是与父域完全隔离的
// 在当前$scope中以已经生明的情况下,$scope.$watch监听的是自身的parentName
$scope.$watch('demo',oblValue) {
console.log('directive-scope -->> from watch scope demo = ' + newValue);
});
// 监听父作用域中的parentName
$scope.$parent.$watch('demo',oldValue) {
console.log('directive-scope -->> from watch parent scope demo = ' + newValue);
});
}]
};
}]);
参数require
与scope参数一样,require参数也是指令与外界通信的媒介。scope参数主要负责的是指令与外界作用域间的通信,require参数主要负责指令与指令之间的通信。大部分自定义指令都很能独立完成某项复杂的任务,往往需要多个指令之间相互组合协作。
require参数接收一个String字符串或是一个字符串数组,String值为需要引入的外界指令的名称,实际传入的是外界指令对应的控制器(这个后续再讨论link参数时详细讨论)。require参数在寻找依赖指令时提供了两种策略’?’ 和 ‘^’。
‘?’ – 如果没有查找到相应的指令,则返回null
‘^’ – 从自身开始并向父级作用域链中搜索依赖指令,返回第一个匹配的值,如果没有’^’则仅仅在自身作用域查找。
行为参数link与controller
link与controller参数都是描述指令行为的参数,但他们两分别负责不同的行为描述。
controller关注的是指令自身内部作用域具备什么样的行为,其关注点在于指令作用域的行为上。
link关注的是指令中HTML模板的操作行为,其关注点在于DOM操作行为上。
link参数理解
从AngularJS官方教程上我们可以获知,Angular在刚从Server Response中获取得到静态网页时,首先回去扫描整个页面并收集HTML页面中包含哪些指令(Angular原生的还是用户自定义的),然后再去加载指令的template中的HTML模板或是下载templateUrl中指定的模板,如此类推,如果加载进来的HTML模板中包含其他指令,继续上诉操作,最终形成模板树,并返回相应的模板函数,提供给下一阶段进行数据绑定。简单的应用示例;
<body ng-app='app'>
<dir-demo></dir-demo> <script > angular.module('app',[]) .directive( 'dirDemo',function () { return { restrict: 'E',template: '<p>dirDemo</p><dirDemo2></dirDemo2>',link: function (scope) { console.log( 'dirDemo2' ); } }; }) .directive( 'dirDemo2',template: '<p>dirDemo2</p><dirDemo3></dirDemo3>',link: function (scope) { console.log( 'dirDemo2' ); } }; }) .directive( 'dirDemo3',template: '<p>dirDemo3</p>',link: function (scope) { console.log( 'dirDemo3' ); } }; }); </script > </ body>
运行上述程序观察输出: dirDemo, dirDemo2, dirDemo3
如果结合代码调试,在link中设置断点,我们可以发现整个执行顺序是:
1 加载模板,形成DOM树
2 执行link函数
3 数据绑定
为什么会是这个执行顺序呢?
其实:在刚形成DOM树的这个时间节点上,进行DOM操作的性能开销是最低的(这事应该是一个DOM片段),进行事件绑定等做操。
angular.module('app',[])
.directive( 'dirDemo',function () {
return {
restrict: 'E',require : '^ngModel',// 需要引用外界指令ngModel的控制器
template: '<p>dirDemo</p><dirDemo2></dirDemo2>',link: function ($scope,ctrl) {
$element.bind("click",function () {
console.log("绑定点击事件");
});
$element.append("<p>增加段落块</p>");
//设置样式
$element.css("background-color","yellow");
//最佳实践中,应该下面的方法转移到controller中
$scope.hello = function () {
console.log("hello");
};
}
};
});
link函数的参数是固定的(
第四个参数即是require参数指定的外部指令的控制器,如果require是一个数组,那么第四个参数相应的也是一个数组,控制器的顺序同require参数中声明的顺序一样。
如果我们在上面的示例代码中添加上controller后,在此进行断点调试可发现,全局的执行顺序是:
1 执行controller,设置各个作用域的scope
2 加载模板,形成DOM模板树
3 执行link函数,设置各级DOM的行为
4 数据绑定,在各级scope中绑定DOM
compile参数
我们通过定义一个compile来取代link函数。可以说compile提供了一个更细粒度的link函数形式,在compile函数中我们可以使用pre-link和post-link函数来替代link函数。
html
<level-one>
<level-two>
<level-three>Hello </level-three>
</level-two>
</level-one>
app.js
function createDirective(name){
return function(){
return {
restrict: 'E',compile: function(tElem,tAttrs){
console.log(name + ': compile');
return {
pre: function(scope,iElem,iAttrs){
console.log(name + ': pre-link');
},post: function(scope,iAttrs){
console.log(name + ': post-link');
}
}
}
}
}
};
angular.module('app',[])
.directive('levelOne',createDirective('levelOne'))
.directive('levelTwo',createDirective('levelTwo'))
.directive('levelThree',createDirective('levelThree'));
运行上面的实例程序,我们能够简单的了解到AngularJS在处理指令时的内部流程。
程序控制台输出为:
levelOne: compile
levelTwo: compile
levelThree: compile
levelOne: pre-link levelTwo: pre-link levelThree: pre-link levelThree: post-link levelTwo: post-link levelOne: post-link
从上面的示例中我们可以发现,link过程,其实还分为pre-link和post-link阶段。同时Angular在指令的link函数调用前先调用了指令的compile函数对指令进行了编译。
参考文献
[AngularJS Developer Guide - Directive][https://docs.angularjs.org/guide/directive]