壹 ❀ 引
我在前面花了三篇文章用于介绍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: '{{vm.name}}', 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: '我的名字是{{vm.name}},我今年{{vm.age}}了', 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: '我的名字是{{vm.name}},我今年{{vm.age}}了', 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: 'parent', 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: 'child', });
在上述代码中,我想在父组件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()">sayNamebutton> <button ng-click="ruin()">destorybutton> 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都能监听销毁,我在组件间也能这么用,那还要什么$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: '我是parent', controller: function ($scope) { var s1 = setInterval(function () { console.log(1); }, 3000); //销毁作用域 this.destroy = function () { $scope.$destroy(); }; //用了scope的销毁监听 $scope.$on('$destroy', function () { clearInterval(s1); }); } }) .component('echo', { template: '我是child', 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