翻译:深入理解Angular 1.5 中的生命周期钩子
文章翻译自:https://toddmotto.com/angular-1-5-lifecycle-hooks
讨论可以访问issue:https://github.com/Jocs/jocs.github.io/issues/3
生命周期钩子是一些简单的函数,这些函数会在Angular应用组件特定生命周期被调用。生命周期钩子在Angular 1.5版本被引入,通常与.component()方法一起使用,并在接下来的几个版本中演变,并包含了更多有用的钩子函数(受Angular 2的启发)。让我们深入研究这些钩子函数并实际使用它们吧。这些钩子函数所带来的作用以及为什么我们需要使用它们,对于我们深入理解通过组件架构的应用具有重要的意义。
在Angular v1.3.0+版本,我自己实现了.component() 方法,该方法深刻得洞悉了怎么去使用这些生命周期函数以及这些函数在组件中的作用,让我们开始研究它吧。
$onInit
什么是$onInit ?首先,他是Angular组件(译注:通过.component() 方法定义的组件)控制器中暴露出来的一个属性,我们可以把一个函数赋值给该属性:
var myComponent = { bindings: {}, controller: function () { this.$onInit = function() { }; }};angular .module('app') .component('myComponent', myComponent);
Using $onInit
$onInit 生命周期钩子用作控制器的初始化工作,下面举个常用例子:
var myComponent = {
...
controller: function () {
this.foo = 'bar';
this.bar = 'foo';
this.fooBar = function () {
};
}
};
注意上面的代码,我们把所有的属性直接赋值到了this上面,它们就像“浮在”控制器的各个角落。现在,让我们通过$onInit 来重写上面代码:
var myComponent = {
...
controller: function () {
this.$onInit = function () {
this.foo = 'bar';
this.bar = 'foo';
};
this.fooBar = function () {
console.log(this.foo); // 'bar'
};
}
};
上面的数据明显地通过硬编码写入的,但是在实际的应用中,我们通常是通过bindings: {} 对象来把我们需要的数据传递到组件中,我们使用$onInit 来进行一些初始化工作,这样就把以前那些“浮在”控制器各处的初始化变量都集中起来了,$onInit 就像是控制器中的constructor ,包含了一些初始化信息。
对于this.fooBar函数呢?不要着急,该函数放在$onInit外面是完全能够访问到初始化数据的,比如当你调用this.fooBar的时候,函数会打印出this.foo的值,也就是在$onInit函数中定义的'bar'。因此所有你初始化的数据都正确地绑定到了控制器的this 上下文中。
$onInit + “require”
因为这些生命周期钩子定义得如此优雅(不同的生命周期钩子都在组件的不同生命周期被调用),一个组件也可以从另外的组件中继承方法,甚至继承的方法在$onInit 钩子中就可以直接使用。
首先我们需要思考的是如何使用require,我写过另外一篇深入介绍$onInit 和 require的文章,但是在此我依然会简要介绍一些require的基本用法,随后将提供一个完整的实例。
让我们来看看myComponent的例子,在这儿require后面紧跟的是一个对象(只在.component()方法中require字段后面接对象),当require和.directive()结合使用的时候,require字段后面也可以跟数组或者字符串语法形式。
var myComponent = {
...
require: {
parent: '^^anotherComponent'
},
controller: function () {
this.$onInit = function () {
this.foo = 'bar';
this.bar = 'foo';
};
this.fooBar = function () {
console.log(this.foo); // 'bar'
};
}
};
如上面的例子,require被设置为^^anotherComponent,require值前面^^表示自会在当前组件的父组件中搜寻anotherComponent控制器,(如果require值前面是^那么首先会在当前组件搜寻是否有该控制器,如果没有再在其父组件中搜寻)这样我们就可以在$onInit中使用任何当定在父组件中的方法了。
var myComponent = {
...
require: {
parent: '^^anotherComponent'
},
controller: function () {
this.$onInit = function () {
this.foo = 'bar';
this.bar = 'foo';
this.parent.sayHi();
};
this.fooBar = function () {
console.log(this.foo); // 'bar'
};
}
};
注意,在Angular 1.5.6版本(见 CHANGELOG)中,如果require对象中属性名和require的控制器同名,那么就可以省略控制器名。这一特性并没有带来给功能带来很大的改变,我们可以如下使用它:
var myComponent = {
...
require: {
parent: '^^'
},
controller: function () {
...
}
};
正如你所见,我们完全省略了需要requre的控制器名而直接使用^^替代。完整写法^^parent就被省略为^^。需要谨记,在前面的一个例子中,我们只能使用parent: '^^anotherComponent'来表示我们需要使用另外一个组件中控制器中的方法(译者注:作者以上就是控制器和requre的属性名不相同时,不能够省略),最后,我们只需记住一点,如果我们想使用该条特性,那么被requre的控制器名必须和require的属性名同名。
Real world $onInit + require
让我们使用$onInit和require来实现一个tabs组件,首先我们实现的组件大概如如下使用:
Tab 1 contents!Tab 2 contents!Tab 3 contents!
这意味着我们需要两个组件,tab和tabs。我们将transclude所有的tabs子元素(就是所有tab模板中的tabs元素)然后通过bindings绑定的对象来获取label值。
首先,组件定义了每个组件都必须使用的一些属性:
var tab = {
bindings: {},
require: {},
transclude: true,
template: ``,
controller: function () {}
};
var tabs = {
transclude: true,
template: ``,
controller: function () {}
};
angular
.module('app', [])
.component('tab', tab)
.component('tabs', tabs);
tab组件需要通过bindings绑定一些数据,同时在该组件中,我们使用了require,transclude和一个template ,最后是一个控制器controller。
tabs组件首先会transclude所有的元素到模板中,然后通过controller来对tabs进行管理。
让我们来实现tab组件的模板吧:
var tab = { ... template: ` `, ...};
对于tab组件而言,我们只在$ctrl.tab.selected为true的时候显示该组件,因此我们需要一些在控制器中添加一些逻辑来处理该需求。随后我们通过transclude来对tab组件中的内容填充。(这些内容就是展示在不同tab内的)
var tabs = { ... template: ` `, ...};
对于tabs组件,我们创建一个数组来展示$ctrl.tabs内容,并对每一个tab选项卡绑定click事件处理函数$ctrl.selectTab(),在调用该方法是传入当前$index。同时我们transclude所有的子节点(所有的元素)到.tabs_content容器中。
接下来让我们来处理tab组件的控制器,我们将创建一个this.tab属性,当然初始化该属性应该放在$onInit钩子函数中:
var tab = {
bindings: {
label: '@'
},
...
template: `
`,
controller: function () {
this.$onInit = function () {
this.tab = {
label: this.label,
selected: false
};
};
}
...
};
你可以看到我在控制器中使用了this.label,因为我们在组件中添加了bindings: {label: '@'},这样我们就可以使用this.label来获取绑定到组件label属性上面的值了(字符串形式)。通过这样的绑定形式我们就可以把不同的值映射到不同的tab组件上。
接下来让我们来看看tabs组件控制器中的逻辑,这可能稍微有点复杂:
var tabs = {
...
template: `
`,
controller: function () {
this.$onInit = function () {
this.tabs = [];
};
this.addTab = function addTab(tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab(index) {
for (var i = 0; i
我们在$onInit钩子处理函数中初始化this.tabs = [],我们已经知道$onInit用来初始化属性值,接下来我们定义了两个函数,addTab和selectTab。addTab函数我们会通过require传递到每一个子组件中,通过这种形式来告诉父组件子组件的存在,同时保存一份对每个tab的引用,这样我们就可以通过ng-repeat来遍历所有的tab选项卡,并且可以点击(通过selectTab)选择不同的选项卡。
接下来我们通过tab组件的require来将addTab方法委派到tab组件中使用。
var tab = {
...
require: {
tabs: '^^'
},
...
};
正如我们在文章关于$onInit和require部分提到,我们通过^^来只requre父组件控制器中的逻辑而不在自身组件中寻找这些方法。除此之外,当我们require的控制器名和requre对象中的属性名相同时我们还可以省略requre的控制器名字,这是版本1.5.6新增加的一个特性。关于这一新特性准备好了吗?在下面代码中,我们使用tabs: '^^',我们有一个和require控制器同名的属性名{tabs: ...},这样我们就可以在$onInit中使用this.tabs来调用父组件控制器中的方法了。
var tab = {
...
require: {
tabs: '^^'
},
controller: function () {
this.$onInit = function () {
this.tab = {
label: this.label,
selected: false
};
// this.tabs === require: { tabs: '^^' }
this.tabs.addTab(this.tab);
};
}
...
};
把所有代码放一起:
var tab = {
bindings: {
label: '@'
},
require: {
tabs: '^^'
},
transclude: true,
template: `
`,
controller: function () {
this.$onInit = function () {
this.tab = {
label: this.label,
selected: false
};
this.tabs.addTab(this.tab);
};
}
};
var tabs = {
transclude: true,
controller: function () {
this.$onInit = function () {
this.tabs = [];
};
this.addTab = function addTab(tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab(index) {
for (var i = 0; i
`
};
点击选项卡相应内容就会呈现出来,当时,我们并没有设置一个初始化的展示的选项卡?这就是接下来$postLink要介绍的内容。
$postLink
我们已经知道,compile函数会返回一个pre和post‘链接函数’,如如下形式:
function myDirective() {
restrict: 'E',
scope: { foo: '=' },
compile: function compile($element, $attrs) {
return {
pre: function preLink($scope, $element, $attrs) {
// access to child elements that are NOT linked
},
post: function postLink($scope, $element, $attrs) {
// access to child elements that are linked
}
};
}
}
你也可能知道如下:
function myDirective() {
restrict: 'E',
scope: { foo: '=' },
link: function postLink($scope, $element, $attrs) {
// access to child elements that are linked
}
}
当我们只需要使用postLink函数的时候,上面两种形式效果是一样的。注意我们使用的post: function)() {...} - 这就是我们的主角。我已经在上面的代码中添加了一行注释“可以获取到已经链接的子元素”,上面的注释意味着在父指令的post 函数中,子元素的模板已经被编译并且已经被链接到特定的scope上。而通过compile和pre函数我们是无法获取到已经编译、链接后的子元素的。因此我们有一个生命周期钩子来帮我我们在编译的最后阶段(子元素已经被编译和链接)来处理一些相应逻辑。
Using $postLink
$postLink给予了我们处理如上需求的可能,我们不需使用一些hack的范式就可以像如下形式一样使用$postLink钩子函数。
var myComponent = {
...
controller: function () {
this.$postLink = function () {
// fire away...
};
}
};
我们已经知道,$postLink是在所有的子元素被链接后触发,接下来让我们来实现我们的tabs组件。
Real world $postLink
我们可以通过$postLink函数来给我们的选项卡组件一个初始的选项卡。首先我们需要调整一下模板:
...
...
...
现在我们就可以通过bindings获取到selected特性的值,然后用以初始化:
var tabs = {
bindings: {
selected: '@'
},
...
controller: function () {
this.$onInit = function () {
this.tabs = [];
};
this.addTab = function addTab(tab) {
this.tabs.push(tab);
};
this.selectTab = function selectTab(index) {
for (var i = 0; i
现在我们已经有一个生动的实例,通过selected属性来预先选择某一模板,在上面的例子中我们使用selected=2来预先选择第三个选项卡作为初始值。
What $postLink is not
在$postLink函数中并不是一个好的地方用以处理DOM操作。在Angular生态圈外通过原生的事件绑定来为HTML/template扩展行为,Directive依然是最佳选择。不要仅仅将Directive(没有模板的指令)重写为component组件,这些都是不推荐的做法。
那么$psotLint存在的意义何在?你可能想在$postLink函数中进行DOM操作或者自定义的事件。其实,DOM操作和绑定事件最好使用一个带模板的指令来进行封装。正确地使用$postLink,你可以把你的疑问写在下面的评论中,我会很乐意的回复你的疑问。
$onChanges
这是一个很大的部分(也是最重要的部分),$onChanges将和Angular 1.5.x中的组件架构及单向数据流一起讨论。一条金玉良言:$onChanges在自身组件被改变但是却在父组件中发生的改变(译者注:其实作者这儿说得比较含糊,$onChange就是在单向数据绑定后,父组件向子组件传递的数据发生改变后会被调用)。当父组件中的一些属性发生改变后,通过bindings: {}就可以把这种变化传递到子组件中,这就是$onChanges的秘密所在。
What calls $onChanges?
在以下情况下$onChanges会被调用,首先,在组件初始化的时候,组件初始化时会传递最初的changes对象,这样我们就可以直接获取到我们所需的数据了。第二种会被调用的场景就是只当单向数据绑定he @(用于获取DOM特性值,这些值是通过父组件传递的)改变时会被调用。一旦$onChanges被调用,你将在$onChanges的参数中获取到一个变化对象,我们将在接下来的部分中详细讨论。
Using $onChanges
使用$onChanges相当简单,但是该生命周期钩子又通常被错误的使用或谈论,因此我们将在接下来的部分讨论$onChanges的使用,首先,我们声明了一个childConpoment组件。
var childComponent = {
bindings: { user: '
注意,这儿bindings对象包含了一个值为'的user字段,该‘表示了单向数据流,这一点在我以前的 文章已经提到过,单向数据流会导致$onChanges钩子被调用。
但是,正如上面提到,我们需要一个parentComponent组件来完成我的实例:
var parentComponent = {
template: `
`
};
angular
.module('app')
.component('parentComponent', parentComponent);
需要注意的是:组件在组件中渲染,这就是为什么我们能够初始化一个带有数据的控制器,并且把这些数据传递给childComponent:
var parentComponent = {
template: `
Change user (this will call $onChanges in child)
`,
controller: function () {
this.$onInit = function () {
this.user = {
name: 'Todd Motto',
location: 'England, UK'
};
};
this.changeUser = function () {
this.user = {
name: 'Tom Delonge',
location: 'California, USA'
};
};
}
};
再次,我们使用$onInit来定义一些初始化数据,把一个对象赋值给this.user。同时我们有this.changeUser函数,用来更新this.user的值,这个改变发生在父组件,但是会触发子组件中的$onChange钩子函数被调用,父组件的改变通过$onChanges来通知子组件,这就是$onChanges的作用。
现在,让我们来看看childComponent组件:
var childComponent = {
bindings: {
user: '
{{ $ctrl.user | json }}
`,
controller: function () {
this.$onChanges = function (changes) {
this.user = changes;
};
}
};
这儿,我们使用binding: {user: ',意味着我们可以通过user来接收来自父组件通过单向数据绑定传递的数据,我们在模板中通过this.user来展示数据的变化,(我通过使用| json过滤器来展示整个对象)
点击按钮来观察childCompoent通过$onChanges来传播的变化:“我并没有获取到变化??”像上面的代码,我永远也获取不到,因为我们把整个变化对象都赋值给了this.user,让我们修改下上面的代码:
var childComponent = {
...
controller: function () {
this.$onChanges = function (changes) {
this.user = changes.user.currentValue;
};
}
};
现在我们可以使用user属性来获取到从父组件传递下来的数据,通过curentValue来引用到该数据,也就是change对象上面的curentChange属性,尝试下上面的代码:
Cloning “change” hashes for “immutable” bindings
现在我们已经从组件中获取到从单向数据绑定的数据,我们可以在深入的思考。虽然单项数据绑定并没有被Angular所$watch,但是我们是通过引用传递。这意味着子组件对象(特别注意,简单数据类型不是传递引用)属性的改变依然会影响到父组件的相同对象,这就和双向数据绑定的作用一样了,当然这是无意义的。这就是,我们可以通过设计。聪明的通过深拷贝来处理单向数据流传递下来的对象,来使得该对象成为“不可变对象”,也就是说传递下来的对象不会在子组件中被更改。
这个是一个fiddle例子(注意user | json)过滤器移到了父组件中(注意,父组件中的对象也随之更新了)
作为替换,我们可以使用 angular.cocy()来克隆传递下来的对象,这样就打破了JavaScript对象的“引用传递“:
var childComponent = {
...
controller: function () {
this.$onChanges = function (changes) {
this.user = angular.copy(changes.user.currentValue);
};
}
};
做得更好,我们添加了if语句来检测对象的属性是否存在,这是一个很好的实践:
var childComponent = {
...
controller: function () {
this.$onChanges = function (changes) {
if (changes.user) {
this.user = angular.copy(changes.user.currentValue);
}
};
}
};
甚至我们还可以再优化我们的代码,因为当父组件中数据发生变化,该变化会立即反应在this.user上面,随后我们通过深拷贝changes.user.currentValue对象,其实这两个对象是相同的,下面两种写法其实是在做同一件事。
this.$onChanges = function (changes) {
if (changes.user) {
this.user = angular.copy(this.user);
this.user = angular.copy(changes.user.currentValue);
}
};
我更偏向于的途径(使用angular.copy(this.user))。
现在就开始尝试,通过深拷贝开复制从父组件传递下来的对象,然后赋值给子组件控制器相应属性。
感觉还不错吧?现在我们使用拷贝对象,我们可以任意改变对象而不用担心会影响到父组件(对不起,双向数据绑定真的不推荐了!)因此当我们更新数据后,通过事件来通知父组件,单向数据流并不是生命周期钩子的一部分,但是这$onChanges钩子被设计出来的意思所在。数据输入和事件输出(输入 = 数据, 输出 = 事件),让我们使用它吧。
One-way dataflow + events
上面我们讨论了bindings和$onChanges已经覆盖了单向数据流,现在我们将添加事件来扩展这一单向数据流。
为了使数据能够回流到 parentComponent,我们需要委托一个函数作为事件的回调函数,然我们添加一个叫updateUser的函数,该函数需要一个event最为传递回来的参数,相信我,这样做将会很有意义。
var parentComponent = {
...
controller: function () {
...
this.updateUser = function (event) {
this.user = event.user;
};
}
};
从这我们可以看出,我们期待event是一个对象,并且带有一个user的属性,也就是从子组件传递回来的值,首先我们需要把该事件回调函数传递到子组件中:
var parentComponent = {
template: `
...
`,
controller: function () {
...
this.updateUser = function (event) {
this.user = event.user;
};
}
};
注意我创建了一个带有on-*前缀的特性,当我们需要绑定一个事件(想想 onclick/onblur)的时候,这是一个最佳实践。
现在我们已经将该函数传递给了,我们需要通过bindings来获取这一绑定的函数。
var childComponent = {
bindings: {
user: '
通过&,我们可以传递函数,所以我们通过this.updateUser字面量来把该函数从父组件传递到子组件,在子组件中更新的数据(通过在$onChanges中深拷贝从bindings对象中的属性)然后通过传递进来的回调函数来将更新后的数据传递回去,数据从父组件到子组件,然后通过事件回调将更新后的数据通知到父组件。
接下来,我们需要扩展我们的模板来时的用户可以更新深拷贝的数据:
var childComponent = {
...
template: `
Update
`,
...
};
这意味着我们需要在控制器中添加this.saveUser方法,让我们添加它:
var childComponent = {
...
template: `
Update
`,
controller: function () {
...
this.saveUser = function () {
};
}
};
尽管,当我们在子组件中"保存"的时候,这其实仅仅是父组件回调函数的一个封装,因此我们在子组件中直接调用父组件方法this.updateUser(该方法已经绑定到了子组件onUpdate属性上)
var childComponent = {
...
controller: function () {
...
this.saveUser = function () {
// function reference to "this.updateUser"
this.onUpdate();
};
}
};
好的,相信我,我们已经到了最后阶段,这也会使得事情变得更加有趣。相反我们并不是直接把this.user传递到回调函数中,而是构建了一个$event对象,这就像Angular 2一样(使用EventEmitter),这也提供了在模板中使用$ctrl.updateUser($event)来获取数据的一致性,这也就可以传递给子组件,$event参数在Angular中是真实存在的,你可以通过ng-submit等指令来使用它,你是否还记得如下函数:(译者注:上面这一段翻译需要推敲)
this.updateUser = function (event) {
this.user = event.user;
};
我们期待event对象带有一个user的属性,好吧,那就让我们来在子组件中saveUser方法中添加该属性:
var childComponent = {
...
controller: function () {
...
this.saveUser = function () {
this.onUpdate({
$event: {
user: this.user
}
});
};
}
};
上面的代码看上去有些怪异。也许有一点吧,但是他是始终一致的,当你使用十遍以后,你就再也不会停止使用它了。必要的我们需要在子组件中创建this.saveUser,然后在该方法中调用从父组件中通过bindings传递进来的this.updateUsser,接着我们传递给它event对象,来把我们更新后的数据返回给父组件:
尝试如上方式写代码吧:
这儿也有一个免费的教学视频,是我关于$onChanges和单向数据流教程的一部分,你可以从这获取到 check it out here.
Is two-way binding through “=” syntax dead?
是的,单向数据绑定已经被认为是数据流的最佳方式,React,Angular 2 以及其他的类库都是用单向数据流,现在轮到Angualr 1了,虽然Angular 1加入单向数据流有些晚,但是依然很强大并将改变Angular 1.x应用开发方式。
Using isFirstChange()
$onChanges还有一个特性,在changeshash对象中,该对象其实是SimpleChange构造函数的一个实例,该构造函数原型对象上有一个isFirstChange方法。
function SimpleChange(previous, current) {
this.previousValue = previous;
this.currentValue = current;
}
SimpleChange.prototype.isFirstChange = function () {
// don't worry what _UNINITIALIZED_VALUE is :)
return this.previousValue === _UNINITIALIZED_VALUE;
};
这就是变化对象根据不同的绑定策略怎么被创造出来(通过 new关键字)(我以前实现过单向数据绑定,并享受这一过程)
你为什么会想着使用该方法呢?上面我们已经提到过,$onChanges会在组件的某给生命周期阶段被调用,不仅在父组件的数据改变时,(译者注:也在数据初始化的时候也会被调用)因此我们可以通过该方法(isFirstChange)来判断是非需要跳过初始化阶段,我们可以通过在改变对象的某属性上面调用isFirstChange方法来判断$onChanges是否是第一次被调用。
this.$onChanges = function (changes) {
if (changes.user.isFirstChange()) {
console.log('First change...', changes);
return; // Maybe? Do what you like.
}
if (changes.user) {
this.user = angular.copy(this.user);
}
};
Here’s a JSFiddle if you want to check the console.
$onDestroy
我们最后来讨论下最简单的一个生命周期钩子,$onDestroy:
function SomeController($scope) {
$scope.$on('$destroy', function () {
// destroy event
});
}
Using $onDestroy
你可以猜想到该生命周期钩子怎么使用:
var childComponent = {
bindings: {
user: '
如果你使用了$postLink来设置了DOM事件监听函数或者其他非Angular原生的逻辑,在$onDestroy中你可以把这些事件监听或者非原生逻辑清理干净。
Conclusion
Angular 1.x 应用开发者的的开发模式也随着单向数据流,生命周期事件及生命周期钩子函数的出现而改变,不久将来我将发布更多关于组件架构的文章。
关键字:angularjs
版权声明
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处。如若内容有涉嫌抄袭侵权/违法违规/事实不符,请点击 举报 进行投诉反馈!