了解angularjs中的生命周期鈎子函數$onInit,$onChange,$onDestory,$postLink


 壹 ❀ 引

我在前面花了三篇文章用於介紹angularjs的指令directive,組件component,並專門花了一篇文章介紹directive與component的不同,其中提到在component的聲明周期中需要配合鈎子函數來實現組件部分功能,例如在bindings傳值過程中,你得通過$onInit方法來初始化數據,那么我們就來好好聊聊component中常用的幾個鈎子函數,本文開始。

 貳 ❀ $onInit

在介紹component的文章中已經有涉及$onInit方法的說明,$onInit用於在component的controller中做數據初始化的操作。

常理上來說,即便我們不通過$onInit為組件綁定數據也是沒問題的,看個簡單的例子:

<div ng-controller="myCtrl">
    <echo></echo>
</div>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {})
    .component('echo', {
        template: '<div>{{vm.name}}</div><button ng-click="vm.sayName()">點我</button>',
        controllerAs: 'vm',
        controller: function () {
            this.name = '聽風是風';
            this.sayName = function () {
                console.log(this.name);
            };
        }
    });

可如果我們需要使用bindings傳遞父作用域的數據,或者利用require注入上層組件的controller時,就一定得使用$onInit方法才能拿到傳遞過來的數據,來看個例子:

<div ng-controller="myCtrl">
    <jack>
        <echo my-name="{{name}}"></echo>
    </jack>
</div>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {
        $scope.name = '時間跳躍';
    })
    .component('jack', {
        controller: function () {
            this.name = '聽風是風';
        }
    })
    .component('echo', {
        controllerAs: 'vm',
        require: {
            jack: '^^'
        },
        bindings: {
            myName: '@'
        },
        controller: function () {
            console.log(this.myName); //undefined
            console.log(this.jack); //undefined
            this.$onInit = function () {
                console.log(this.myName); //時間跳躍
                console.log(this.jack); // controller {name: "聽風是風"}
            }
        }
    });

在上面的例子中我分別在父作用域上綁定了一個name屬性,並通過bindings傳遞給組件echo,並注入了父組件jack的控制器,可以看到只有在$onInit中才能正確的拿到它們,這就是$onInit的作用。

 叄 ❀ $onChanges

事實上$onInit的初始化只會執行一遍,如果我們通過bindings傳入了父作用域中的數據,父作用域的數據改變其實子組件是無法感知的,我們看個例子:

<div ng-controller="myCtrl">
    <div>我的名字是{{myself.name}},我今年{{myself.age}}了</div>
    <echo my-age="myself.age" my-name="myself.name"></echo>
    <button ng-click="reduce()">一鍵返老還童</button>
</div>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {
        $scope.myself = {
            name: '聽風是風',
            age: 26
        };
        $scope.reduce = function () {
            $scope.myself.age--;
        };
    })
    .component('echo', {
        controllerAs: 'vm',
        bindings: {
            myName: "<",
            myAge: '<'
        },
        template: '<div>我的名字是{{vm.name}},我今年{{vm.age}}了</div>',
        controller: function () {
            this.$onInit = function () {
                this.name = this.myName;
                this.age = this.myAge;
            };
        }
    });

我將父作用域對象中一條屬性通過bindings傳遞給子組件后,通過點擊事件不斷減小age屬性,可以看到組件中沒辦法感知到變化。那么這種情況就可以利用$onChanges實現,我們修改代碼:

angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {
        $scope.myself = {
            name: '聽風是風',
            age: 26
        };
        $scope.reduce = function () {
            $scope.myself.age--;
        };
    })
    .component('echo', {
        controllerAs: 'vm',
        bindings: {
            myName: "<",
            myAge: '<'
        },
        template: '<div>我的名字是{{vm.name}},我今年{{vm.age}}了</div>',
        controller: function () {
            this.$onInit = function () {
                this.name = this.myName;
            };
            this.$onChanges = function (changes) {
                console.log(changes);
                this.age = changes.myAge.currentValue;
            };
        }
    });

有人肯定會問了,這個$onChanges這么好用,那能不能直接取代$onInit做初始化呢,這個changes參數又是啥,我們可以打印它:

 

changes代表的正是bindings中變化的變量,這里一共輸出了2次,第一次是組件初始化時myName與myAge從無到有,第二次輸出是因為點擊導致age減少,由此可見$onChanges只能監聽到bindings中變化的變量,並不適合做初始化。

 肆 ❀ $postLink

我們知道directive因為編譯函數與鏈接函數的存在,我們可以在DOM編譯階段操作DOM以及鏈接階段綁定數據,而component提供的$postLink方法可在組件自身模板和其子級組件模板已鏈接之后被調用。

通過angularjs生命周期我們知道,組件總是先編譯完成,再將模板與scope鏈接,且鏈接過程是從子往父回溯綁定,由這一點可以確定$postLink執行時子組件的模板一定已經編譯完成,我們來看個例子:

<div ng-controller="myCtrl">
    <jack>
        <echo></echo>
    </jack>
</div>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope) {})
    .component('jack', {
        transclude: true,
        template: '<div>parent</div><div ng-transclude></div>',
        controller: function ($scope) {
            var child = document.querySelector('#child');
            console.dir(child);//?
            this.$postLink = function () {
                var child = document.querySelector('#child');
                console.dir(child);//?
            };

        }
    })
    .component('echo', {
        template: '<div id="child">child</div>',
    });

在上述代碼中,我想在父組件jack的控制器中獲取子組件id為child的元素,於是我分別在控制器外層與$postLink中分別獲取兩次,並打印它們,可以看到$postLink中成功獲取到了子模板元素。

我們根據斷點查看DOM變化,在打印第一個時,子組件模板還沒加載,而跑到$postLink時,子組件模板已經加載完畢了,所以此時我們可以獲取到正確的DOM。

 伍 ❀ $onDestroy

$onDestroy用於在作用域被銷毀時,用於清除掉那些我們先前自定義的事件監聽或者定時器等。

我們知道angularjs在$scope上提供了一個$destory方法用於主動銷毀當前作用域,對於這個方法陌生可以看看angularjs權威指南的解釋:

 我們知道就算是JS用於垃圾回收機制在操作DOM或者定時器時也得在必要的時候手動釋放它們,angularjs雖然也存在自動清理作用域的情況,但它也沒辦法銷毀我們定義的原生JS邏輯,來看個例子:

<div ng-controller="myCtrl">
    <button ng-click="sayName()">sayName</button>
    <button ng-click="ruin()">destory</button>
</div>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope, $interval) {
        $scope.ruin = function () {
            console.log('銷毀作用域');
            //銷毀作用域
            $scope.$destroy();
        };
        $scope.sayName = function () {
            console.log('聽風是風');
        };

        //原生定時器
        var s1 = setInterval(function () {
            console.log('我是原生定時器');
        }, 3000);
        //angular提供的定時器
        var s2 = $interval(function () {
            console.log('我是angular定時器');
        }, 3000);
    })

在這個例子中,我們分別用創建了一個原生定時器與angular的封裝定時器,在銷毀作用域后可以看到綁定的sayName失效,但兩個定時器仍然在起作用。怎么感知銷毀並清除掉2個定時器呢,angular是這么做的,添加如下代碼:

$scope.$on('$destroy', function () {
    clearInterval(s1);
    $interval.cancel(s2);
})

可以看到我們通過$on監聽銷毀后清除了定時器起到了作用,之后定時器並未執行,所以當我們想要監聽摧毀發生后去做一些事情時,利用on監聽銷毀是個不錯的選擇。

那么說道這你肯定就疑問了,$on都能監聽銷毀,我在組件間也能這么用,那還要什么$onDestroy鈎子函數,其實我一開始也有這個疑問,我在谷歌百度了相關資料,唯一得的合理就是,脫離$scope的約束。

我們知道在angularjs早期版本數據都是推崇綁定scope上,在后面版本我們可以通過as vm的做法將數據綁定在控制器this上,而組件的出現,你會發現組件傳值都是默認綁定在控制器this上,也就是說在組件component開發中我們不用注入$scope都能做到任意數據綁定,而鈎子函數$onDestroy的出現真是滿足了這一點。

實際開發中我們很少使用$scope.$destroy()手動銷毀作用域,我們要做的僅僅是感知銷毀並做要做的事情而已,來看個例子:

<body ng-controller="myCtrl">
    <jack>
        <echo></echo>
    </jack>
</body>
angular.module('myApp', [])
    .controller('myCtrl', function ($scope, $interval) {})
    .component('jack', {
        transclude: true,
        template: '<div>我是parent</div><button ng-click="$ctrl.destroy()">destory</button><div ng-transclude></div>',
        controller: function ($scope) {
            var s1 = setInterval(function () {
                console.log(1);
            }, 3000);
            //銷毀作用域
            this.destroy = function () {
                $scope.$destroy();
          console.log('銷毀父作用域'); };
//用了scope的銷毀監聽 $scope.$on('$destroy', function () { clearInterval(s1); }); } }) .component('echo', { template: '<div>我是child</div>', controller: function () { this.s2 = setInterval(function () { console.log(1); }, 3000); //用了鈎子函數的監聽 this.$onDestroy = function () { clearInterval(this.s2); }; } });

這里我創建了父組件jack與子組件echo,並分別用scope與鈎子函數的作用域銷毀監聽方法,可以看到一旦父組件作用域銷毀,父子組件中的監聽函數都起到了作用。

但按照component擁有隔離作用域的特點,銷毀父組件作用域應該不會影響子組件才對,所以這里的效果反而解釋不通;我在上一個例子中銷毀了外層控制器的作用域確實沒對組件造成影響。

 陸 ❀ 總

其實文章說了這么多,到頭來發現只有$onInit與$onChange在實際開發中會很有用,另外兩個方法非常冷門,以至於在查閱資料時非常吃力,但站在了解的角度也是不錯的,萬一以后有需求需要使用呢?

如果對於angularjs指令,組件以及它們區別有興趣,可以閱讀博主相關文章:

angularjs 一篇文章看懂自定義指令directive

一篇文章看懂angularjs component組件

angularjs中directive指令與component組件有什么區別?

angularjs $scope與this的區別,controller as vm有何含義?

那么本文到這里結束。

 參考

翻譯:深入理解Angular 1.5 中的生命周期鈎子

AngularJS: $onDestroy component hook makes $scope unnecessary in hybrid Cordova mobile application events unbinding and in interval/timeout cleaning

$postLink of an angular component/directive running too early


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM