壹 ❀ 引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有何含義?
那麼本文到這裏,結束。