#2 组件生命周期
英文原版:https://guides.emberjs.com/v2.13.0/components/the-component-lifecycle/
组件最吸引人的就是它允许你介入DOM。这允许你可以直接对DOM进行操纵,监听和响应浏览器事件,并且你可以在Ember应用中加入第三方JS库。
当组件被初次渲染,重新渲染直至最后被移除,Ember提供了一系列的生命周期钩子可以使你在组件特定的生命周期执行特定的代码。
为了最大限度的利用组件,理解这些生命周期方法是很重要的!!!
生命周期钩子调用顺序
下面的列表罗列了在组件渲染时调用的生命周期函数:
初次渲染
- init
- didReceiveAttrs
- willRender
- didInsertElement
- didRender
重新渲染
- didUpdateAttrs
- didReceiveAttrs
- willUpdate
- willRender
- didUpdate
- didRender
销毁
- willDestroyElement
- willClearRender
- didDestroyElement
生命周期钩子示例
下面是一些简单使用生命周期函数的例子
通过didUpdateAttrs改变属性的状态
didUpdateAttrs会在组件的属性变化时执行,但是不会在使用component.rerender, component.set重新渲染组件时执行,也不会因为在模板的model改变和servicese的影响而执行。
由于didUpdateAttrs是在重新渲染前被调用,所以你可以利用此钩子函数在某个属性变化时执行一些操作。这个钩子可以很好的代替observer,因为它会在重新渲染前、属性变化后执行。
一个比较贴切的例子是一个输入信息组件。比方说你在编辑一个用用户,并且用户属性变化了,这时候你可以通过didUpdateAttrs钩子来清除之前填错信息时弹出的错误信息。
/app/templates/components/profile-editor.hbs
<ul class="errors">
{{#each errors as |error|}}
<li>{{error.message}}</li>
{{/each}}
</ul>
<fieldset>
{{input name="user.name" value=name change=(action "required")}}
{{input name="user.department" value=department change=(action "required")}}
{{input name="user.email" value=email change=(action "required")}}
</fieldset>
/app/components/profile-editor.js
import Ember from 'ember';
export default Ember.Component.extend({
init() {
this._super(...arguments);
this.errors = [];
},
didUpdateAttrs() {
this._super(...arguments);
this.set('errors', []);
},
actions: {
required(event) {
if (!event.target.value) {
this.get('errors').pushObject({ message: `${event.target.name} is required`});
}
}
}
});
通过didReceiveAttrs来格式化组件属性
didReceiveAttrs 在init( ) 之后运行,并且会在每次的重新渲染中执行,你可以利用它来执行一些对每次渲染都有用的逻辑。不会在内部初始化重新渲染时执行。
由于这个钩子会在组件属性每次变化、渲染、重新渲染时被调用,所以你可以把它当做一个非常敬业的observer,它可以确保在每次属性变化时都执行代码。
例子,假设你有个组件,它的渲染依赖于一串JSON格式的配置数据,不过你希望你的组件以字符串的形式来接收它,那么你就可以使用didReceiveAttrs 来保证JSON数组总是被解析为字符串。
import Ember from 'ember';
export default Ember.Component.extend({
didReceiveAttrs() {
this._super(...arguments);
const profile = this.get('data');
if (typeof profile === 'string') {
this.set('profile', JSON.parse(profile));
} else {
this.set('profile', profile);
}
}
});
在didInsertElement中引入第三方库
假设你现在非常希望在Ember应用中引入你钟爱的某款datepicker。通常,这些第三方的JS/jQuery库都会绑定一个元素。所以,那个地方才是初始化并且使用这些库的绝佳场所呢?
在组件成功的把它背后的HTML元素渲染进DOM后,它就会触发didInsertElement() 钩子。
Ember会确保,在didInsertElement()被调用的时候:
- 组件的元素已经被创建并且插入了DOM。
- 组件的元素可以通过$()方法被操作。
组件的$( )方法可以使你与该方法返回的jQuery对象来交互。比如,你可以通过$( )方法来设置一个属性.。
didInsertElement() {
this._super(...arguments);
this.$().attr('contenteditable', true);
}
$( )将会默认的返回该组件的根元素,但是你也可以通过选择器来操作该组件元素的子元素。
didInsertElement() {
this._super(...arguments);
this.$('div p button').addClass('enabled');
}
让我们通过didInsertElement( )来初始化我们的datepicker。
datepicker插件一般会依附于<input>元素,所以我们将通过jQuery寻找在组件中的适当的input元素。
didInsertElement() {
this._super(...arguments);
this.$('input.date').myDatePickerLib();
}
didInsertElement( )同样也是一个添加事件监听的好地方。尤其是没有对应的内键事件助手的情况下(Ember貌似没有实现所有的浏览器事件助手)
比如,你有一些自定义的CSS动画会在组件渲染时执行,并且你需要在动画结束时做一些收尾工作。
didInsertElement() {
this._super(...arguments);
this.$().on('animationend', () => {
$(this).removeClass('.sliding-anim');
});
}
这里有些使用didInsertElement( )需要注意的事项:
- 它仅会在组件元素初次被渲染时触发。
- 在组件嵌套的情况下,子组件总是会在父组件之前收到didInsertElement( )的调用通过。
- 在didInsertElement( )中设置属性会再次触发这个钩子,所以此行为是禁止的。
- 从技术上讲,didInsertElement( )钩子是可以通过on( )监听的事件, 鼓励覆写它,尤其是在调用顺序比较重要的情况下。
通过didRender来在重新渲染DOM时做些更新
didRender( )钩子会在组件初次渲染和重新渲染期间,模板渲染之后以及DOM更新后被调用。你可以利用这个钩子在组件对应的DOM被更新后来执行一些后处理。
下面的例子,现在有个列表组件,它需要在渲染后滚动到被选择的项。因为滚动到指定的项需要基于这个DOM的位置,所以我们需要确保这个列表在滚动前就已经被渲染好。我们可以先渲染这个列表,再设置滚动。
下面这个组件提供了列表项并且将它们显示在屏幕上。额外的,它提供代表当前被选中的项以及将被选中的项,并且将滚动条移动到此项在页面的位置。
{{selected-item-list items=items selectedItem=selection}}
当组件被渲染时,它将会遍历给定的items,并且给被选中的项加一个class类。
/app/templates/components/selected-item-list.hbs
{{#each items as |item|}}
<div class="list-item {{if item.isSelected 'selected-item'}}">{{item.label}}</div>
{{/each}}
滚动操作会在didRender( )钩子中执行。
/app/components/selected-item-list.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: ['item-list'],
didReceiveAttrs() {
this._super(...arguments);
this.set('items', this.get('items').map((item) => {
if (item.id === this.get('selectedItem.id')) {
item.isSelected = true;
}
return item;
}));
},
didRender() {
this._super(...arguments);
this.$('.item-list').scrollTop(this.$('.selected-item').position.top);
}
});
通过willDestroyElement( )钩子卸载组件
当一个组件检测到它不在被DOM需要,Ember就会触发willDestroyElement( )钩子,使你可以做一些清理工作。
组件的销毁可以通过很多情况触发。比如,用户导航去了别的路由,或者通过{{if}}助手来切换组件。
{{#if falseBool}}
{{my-component}}
{{/if}}
现在让我们通过这个钩子函数来清理datepicker和接触事件监听。
willDestroyElement() {
this._super(...arguments);
this.$().off('animationend');
this.$('input.date').myDatepickerLib().destroy();
}
本节完