AngularJS 学习笔记 -- 指令Directive

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. parent引用父作用域

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函数的参数是固定的( scope, element,ctrl);
第四个参数即是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函数对指令进行了编译。

关于compile的更多内容将在下一篇文章中进行分享

参考文献
[AngularJS Developer Guide - Directive][https://docs.angularjs.org/guide/directive]

相关文章

ANGULAR.JS:NG-SELECTANDNG-OPTIONSPS:其实看英文文档比看中...
AngularJS中使用Chart.js制折线图与饼图实例  Chart.js 是...
IE浏览器兼容性后续前言 继续尝试解决IE浏览器兼容性问题,...
Angular实现下拉菜单多选写这篇文章时,引用文章地址如下:h...
在AngularJS应用中集成科大讯飞语音输入功能前言 根据项目...
Angular数据更新不及时问题探讨前言 在修复控制角标正确变...