壹 ❀ 引html
我在 angularjs 一篇文章看懂自定义指令directive 一文中简单说起了自定义指令中的link连接函数与compile编译函数,并说到二者具备互斥特性,即同时存在link与compile时link不生效。因为上篇博文篇幅问题,实在很差再过多讨论link,compile,那么本文将围绕三个问题展开,一是再识link与compile函数,你将知道二者为什么互斥;二是了解link、compile与controller的区别,存在即合理,在合适的场景下应该使用哪一个方法;三是了解指令中代码执行顺序,link与controller执行关系,多层指令又会如何执行?那么本文开始。angularjs
贰 ❀ directive中的link与compile安全
咱们已经知道编译函数compile与连接函数link互斥,两者只能存在其一,好比下方例子中,link函数并不会执行:dom
angular.module('myApp', []) .controller('myCtrl', function ($scope, $q) {}).directive("echo", function () { return { restrict: 'EA', compile: function () { console.log('开始编译了!'); }, link: function () { console.log('开始给DOM绑定事件数据了!')//不执行 } } })
那这样就产生了一个问题,是否是compile存在就不能操做link函数了?并非这样,完整的compile函数其实自己就包含了link函数,有以下两种写法:函数
写法一:post
angular.module('myApp', []) .controller('myCtrl', function () {}) .directive('echo', function () { return { restrict: 'EACM', scope: {}, replace: true, controller: function ($scope, $element) {}, compile: function (tEle, tAttrs, transcludeFn) { //这里模板编译完成但还没被成功返回,咱们能够对编译后的DOM树加工 console.log('编译完成,加工DOM吧') //返回一个函数做为link函数,模板编译已完成,进入连接阶段 return function postLink(scope, ele, attrs) { console.log('开始执行连接函数link'); }; } } })
写法二:ui
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo', function () { return { restrict: 'EACM', scope: {}, controller: function ($scope, $element) {}, compile: function () { //这里模板编译完成但还没被成功返回,咱们能够对编译后的DOM树加工 console.log('模板编译完成,能够访问使用指令的dom元素以及元素上的属性了') //返回一个对象做为link函数,只是这个link又分为了两个部分 return { pre: function (scope, iElement, iAttrs, controller) { //在子元素被连接以前执行(也就是子元素的postLink执行以前),这里执行DOM转换和连接函数不安全 console.log('pre开始执行了'); }, post: function (scope, iElement, iAttrs, controller) { // 在子元素被连接以后执行,在这里执行DOM转换和连接函数同样安全 console.log('link开始执行了'); } } } } })
在上面这个例子中,compile返回的整个对象做为link函数,只是link函数又分为了pre与post两个阶段,我暂且称为preLink与postLink函数,其中postLink对应的就是咱们熟悉的Link函数。this
postLink咱们知道是在DOM元素连接阶段完成以后执行,而preLink有点特殊,它是在全部指令模板编译完成且子指令postLink执行以前执行(也就是子指令连接阶段以前),虽然preLink函数中也能给指令模板绑定数据方法,但通常不推荐使用preLink函数,要么使用postLink,或者不写compile直接使用Link函数。spa
我知道你这里必定有疑问了,preLink和postLink都能给指令绑定事件监听DOM,官方为啥不推荐使用preLink,没用设计它干吗,两者真就一点区别也没有?固然有,这个咱们得先介绍angular的生命周期,不了解这个还真很差解释。设计
肆 ❀ angular生命周期
经过上文的compile与link了解,咱们大体知道了angular生命周期中存在编译阶段与连接阶段两个重要阶段。angular的指令在angular启动前,会以普通文本形式保存在HTML中,但当angular正式启动,这些指令就会经历编译与连接。
1.编译阶段
在编译阶段angular会找到指令,若指令存在模板则开始编译解析模板,但有个问题,指令模板中也可能存在模板,因而还得编译指令模板中子指令的模板,相似与深度遍历。
一旦指令DOM编译完成,模板就会返回一个模板函数,咱们有机会在指令的模板函数被返回前对编译后的DOM树进行修改,这个机会就在咱们前面说的compile函数里。
直到指令和子指令模板DOM编译完成,最外层的父指令模板会统一返回一个模板函数,待模板函数返回完成,编译阶段正式结束。
因为compile处于DOM解析完成且模板函数还未成功返回的阶段,因此compile函数执行必定与编译顺序保持一致,知足从上到下,从外到内的前后顺序执行,咱们来看例子:
<body ng-controller="myCtrl"> <div echo1></div> <div echo3></div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo1', function () { return { restrict: 'EACM', template:'<span><echo2></echo2></span>', compile: function () { console.log('compile1开始执行'); } } }) .directive('echo2', function () { return { restrict: 'EACM', compile: function () { console.log('compile2开始执行'); } } }) .directive('echo3', function () { return { restrict: 'EACM', compile: function () { console.log('compile3开始执行'); } } })
上述例子中,指令echo1拥有子指令echo2与兄弟DOM指令echo3,直到echo2编译完成,echo3才能编译,那么咱们知道compile执行与编译阶段保持一致,知足从上到下,从父到子深度遍历的顺序。
因为compile能够对编译出来的DOM进行再加工,因此最终编译出来的DOM树可能与你模板中的DOM结构不一致,所以不推荐在compile阶段作监听DOM事件的操做。
2.连接阶段
在compile执行结束,模板函数被返回并传递给了指令中定义的link函数,此时开始连接阶段;连接阶段负责将编译阶段编译好的DOM树与scope相关联,这样link函数就能将定义好的数据,事件与DOM绑定在一块儿,实现DOM操做与监听。
前面也说了指令也会有子指令,而这个preLink则在编译完成(compile)以后子指令连接以前(preLink)执行,因此preLink也在compile以后,且在子指令的preLink与postLink以前执行。
postLink比较特殊,postLink永远在编译完成且子指令连接以后执行(postLink以后),因此也是在compile以后,且在子指令的postLink以后。
有点混乱了,理一理,以单个指令来讲,它应该是编译阶段开始---DOM编译成功执行compile---返回模板函数(编译结束)---模板函数传递给link---连接阶段开始,DOM与scope关联---执行pre---执行post---连接阶段结束。
而当指令包含子指令时,它应该是编译阶段开始---父指令DOM编译成功执行父compile---返回模板函数---子指令DOM编译成功执行子compile---返回模板函数---模板函数传递给link---连接阶段开始,DOM与scope关联---执行父pre---执行子pre---执行子post---执行父post---连接阶段结束。
看个例子:
<body ng-controller="myCtrl"> <div echo></div> </body>
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo', function () { return { restrict: 'EACM', template:'<span><echo1></echo1></span>', compile: function () { console.log('compile1开始执行'); return { pre: function () { console.log('pre1开始执行'); }, post: function () { console.log('post1开始执行'); } } } } }) .directive('echo1', function () { return { restrict: 'EACM', compile: function () { console.log('compile2开始执行'); return { pre: function () { console.log('pre2开始执行'); }, post: function () { console.log('post2开始执行'); } } } } })
compile与pre就像深度遍历,有子就一直往下执行,post就像回溯,从里往外执行。
3.preLink与postLink的区别
前面咱们留下了一个问题,preLink到底有什么用,咱们来看下面这段代码,猜猜会如何执行:
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo', function () { return { restrict: 'EACM', template: '<span><echo1></echo1></span>', link: function (scope) { scope.name = '听风是风'; } } }) .directive('echo1', function () { return { restrict: 'EACM', template: '<span>{{describe}}</span>', link: function (scope) { scope.describe = '个人名字是' + scope.name; } } })
致使scope.name没法取到值的缘由是,这里的link函数就是咱们以前提到的postLink函数,postLink函数执行就像回溯,子指令先执行,因此取值的时候父指令还未声明此变量。想要作到父指令给子指令做用域传值,preLink就能作到这一点:
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo', function () { return { restrict: 'EACM', template: '<span><echo1></echo1></span>', compile: function () { return { pre: function (scope) { scope.name = '听风是风'; }, post: function () { } } } } }) .directive('echo1', function () { return { restrict: 'EACM', template: '<span>{{describe}}</span>', link: function (scope) { scope.describe = '个人名字是' + scope.name; } } })
那么到这里咱们知道了pre与post的区别,pre能够利用本身执行顺序的优点给子指令做用域直接传值,可是仍然不推荐这么作,这里咱们只是做为知识了解。毕竟指令应该拥有干净隔离的做用域,也不会用到这种传值模式。
叁 ❀ link、compile、controller的职责
那么经过上文的介绍,咱们知道了link与compile对应了连接和编译两个阶段,编译函数负责对模板DOM进行转换,在做用域同DOM连接以前能够手动操做DOM。在开发中编写自定义指令时这种操做是很是罕见,因此compile使用很少。
连接函数负责将做用域和DOM进行连接,编译函数会在模板编译完成并同做用域进行连接后被调用,所以它负责设置事件监听器,监视数据变化和实时的更新DOM,这与controller十分相似。
抛开加工DOM的compile不说,那咱们应该在什么状况下使用controller和link呢,毕竟这两兄弟都能作DOM事件监听与数据更新;其实很简单,若是你但愿这个指令的属性方法能被其它指令复用,那就将方法属性定义在controller中,若是只是但愿给指令本身使用,那就加在link函数中。
之因此这么说,是由于指令有一个require属性,经过require,咱们能将require值同名指令的controller加入到当前指令中,而后就能够经过link函数的第四个参数直接使用被require指令controller中的属性方法了,来看个例子:
angular.module('myApp', []) .controller('myCtrl', function ($scope) {}) .directive('echo', function () { return { restrict: 'EACM', template: '<span><echo1></echo1></span>', controller: function () { this.sayName = function (name) { console.log('个人名字是' + name); } } } }) .directive('echo1', function () { return { restrict: 'EACM', template: '<button ng-click="myName(name)">点我</button>', require: '^echo', link: function (scope, ele, attr, ctrl) { console.log(ctrl); scope.name = '听风是风'; scope.myName = ctrl.sayName; } } })
能够看到在指令echo1中成功注入了指令echo的controller,咱们能在echo1中直接使用echo的方法,这就是为什么说若是指令方法须要复用,建议绑在controller中的缘由。
肆 ❀ 总
那么到这里,咱们知道了compile与link是互斥关系,若是同时写了两个函数,link不会执行,这是由于compile自己就会返回一个函数做为link,哪怕你不写返回函数,那也认定你返回了一个空的link。
其次,咱们知道了pre与post的区别,这两个函数虽然同属于link的一部分,但在生命周期中扮演了不一样的角色,pre在子元素连接完成前执行,而post在子元素连接完成以后,这样致使了父的post反而比子post晚一步执行
咱们简单介绍了angular生命周期中两个重要的过程,编译阶段与连接阶段,经过这两个阶段,也解释了为何compile与pre执行像深度遍历,而post像回溯的缘由。
最后,咱们将controller,link,compile工做职责作了一个简单介绍,link与controller很像,但若是你想指令属性方法复用,推荐绑定在controller上,若是只是指令本身使用,推荐加在link上。
最后我还要留一个疑问,为何在最后的例子中,指令echo1想复用指令echo controller上的方法,方法sayName是绑定在this上的,若是绑在scope上能不能复用呢?angular中的scope和this究竟是什么关系?这个我会在下篇博客中好好介绍。
博客已更新 angularjs $scope与this的区别,controller as vm有何含义?
那么本文到这里,结束。