壹 ❀ 引
我在 angularjs 一篇文章看懂自定義指令directive 一文中簡單提及了自定義指令中的link鏈接函數與compile編譯函數,並說到兩者具有互斥特性,即同時存在link與compile時link不生效。由於上篇博文篇幅問題,實在不好再過多討論link,compile,那么本文將圍繞三個問題展開,一是再識link與compile函數,你將知道兩者為何互斥;二是了解link、compile與controller的區別,存在即合理,在合適的場景下應該使用哪個方法;三是了解指令中代碼執行順序,link與controller執行關系,多層指令又會如何執行?那么本文開始。
貳 ❀ directive中的link與compile
我們已經知道編譯函數compile與鏈接函數link互斥,二者只能存在其一,比如下方例子中,link函數並不會執行:
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函數,有如下兩種寫法:
寫法一:
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'); }; } } })
寫法二:
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函數。
postLink我們知道是在DOM元素鏈接階段完成之后執行,而preLink有點特殊,它是在所有指令模板編譯完成且子指令postLink執行之前執行(這里如果看不懂后面具體會解釋),雖然preLink函數中也能給指令模板綁定數據方法,但一般不推薦使用preLink函數,要么使用postLink,或者不寫compile直接使用Link函數。
我知道你這里一定有疑問了,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 echo2></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有何含義?
那么本文到這里,結束。