组件如此有用的部分原因是它们可以让您完全控制DOM的一部分。这允许直接DOM操作,监听和响应浏览器事件,以及在Ember应用程序中使用第三方JavaScript库。
随着组件的渲染,重新渲染和最终删除,Ember提供了生命周期钩子,允许您在组件的生命周期中的特定时间运行代码。
为了最大限度地利用组件,了解这些生命周期方法非常重要。
被称为生命周期的钩子的顺序
下面列出了根据渲染场景执行顺序的组件生命周期钩子
初始渲染
init
didReceiveAttrs
willRender
didInsertElement
didRender
重新渲染
didUpdateAttrs
didReceiveAttrs
willUpdate
willRender
didUpdate
didRender
组件销毁
willDestroyElement
willClearRender
didDestroyElement
生命周期钩子示例
下面是一些在组件中使用生命周期钩子的样本方法:
使用didUpdateAttrs
重置关于属性变化的表示状态
didUpdateAttrs
在组件的属性发生变化时运行,但在组件重新渲染时,通过component.rerender
,component.set
,或模板使用的models或services更改时运行。
由于在重新渲染之前调用了didUpdateAttrs
,你可以在特定代码更改时使用这个钩子。这个钩子可以成为观察者模式的有效替代品,因为它将在重新渲染之前运行,但在属性发生变化之后。
这种情况的一个例子是配置文件编辑器组件。当你编辑一个用户并更改用户属性的时候,你可以使用didUpdateAttrs
来清除编辑先前用户时建立的任意错误状态。
// in component.js
export default Component.extend({
init() {
this._super(...arguments);
this.set('errors', []);
},
didUpdateAttrs() {
this._super(...arguments);
this.set('errors', []);
}
})
使用didReceiveAttrs
格式化组件
didReceiveAttrs
在init
之后运行,也在后续的重新渲染的时候运行,这对于所有渲染中相同的逻辑很有用。在内部启动重新渲染时,它不会运行。
因为didReceiveAttrs
钩子在每次组件的属性更新时都会被调用,无论是初始渲染还是重新渲染,你可以使用这个钩子有效地充当观察者,确保每次属性更改时都会执行代码。
举例来说,如果你有一个在json配置上渲染的组件,但是你想要给你的组件提供配置为字符串的选项,你可以利用didReceiveAttrs
来确保始终解析传入配置。
//in component.js
export default Component.extend({
didReceiveAttrs() {
this._super(...arguments);
const profile = this.data;
if (typeof profile === 'string') {
this.set('profile', JSON.parse(profile));
} else {
this.set('profile', profile);
}
}
})
使用didInsertElement
与第三方库集成
假设你想集成你最喜欢的日期选择期库到Ember项目。通常情况下,第三方JS/jQuery库请求绑定到一个DOM元素。所以,初始化和附加库的最佳位置在哪里?
在一个组件成功将其支持的HTML元素渲染到DOM中之后,它将会触发 didInsertElement()
钩子。
Ember 保证在调用didInsertElement()
时:
- 组件的元素已经被创建并插入到DOM中。
- 组件的元素可以通过组件的
this.element
属性访问。
element
属性允许你访问组件的DOM元素。例如,你可以使用Element.setAttribute()
方法来设置属性:
//in component.js
export default Component.extend({
didInsertElement() {
this._super(...arguments);
this.element.setAttribute('contenteditable', true);
}
})
默认情况下,element
属性将返回组件根元素的DOM对象,但是你也可以通过将选择器传递给querySelector
或querySelectorAll
来定位组件模板中的子元素:
// in component.js
export default Component.extend({
didInsertElement() {
this._super(...arguement);
let buttons = this.element.querySelectorAll('div p button');
button.forEach(function(button) {
button.classList.add('enabled');
})
}
})
让我们通过覆盖didInsertElemen()
的方法来初始化我们的日期选择器。
日期选择期库通常附加到<input>
元素,所以我们将使用this.element.querySelector
在组件模板中查找适当输入。
// in component.js
didInsertElement() {
this._super(...arguments);
this.datePicker = myDatePickerLib(this.element.querySelectorAll('input.date'));
}
didInsertElement()
也是一个附加事件监听的好地方。这对于没有内置事件处理程序的自定义事件或其他浏览器时间特别有用。
例如,在组件渲染时,你可能会有一些自定义的CSS动画触发器,并且你希望在渲染结束时处理一些清理:
//in component.js
export default Component.extend({
didInsertElement() {
this._super(...arguments);
this.element.addEventListener('animationend', () => {
this.element.classList.remove('sliding-anim')
})
}
})
关于didInsertElement()
钩子有几点需要注意:
- 它仅在组件元素首次渲染时触发一次。
- 如果你将组件嵌套在其他组件中,子组件将始终在它的父组件之前接收
didInsertElement()
调用。 - 在组件中使用
didInsertElement()
设置属性会出发重新渲染,并且为了性能考虑,不允许使用。 - 虽然
didInsertElement()
从技术上讲是一个可以使用on()
监听的事件,但它鼓励覆盖默认方法本身,特别是在执行顺序很重要时。
使用didRender
更新渲染的DOM
在模板渲染和更新DOM之后,在渲染和重新渲染期间调用didRender
钩子。您可以利用此挂钩在组件更新后对组件的DOM执行后处理。
在这个例子中,有一个列表组件需要在渲染时滚动到选定的项目。由于滚动到特定位置是基于DOM中的位置,因此我们需要确保在滚动之前已经渲染了列表。我们可以先渲染此列表,然后设置滚动。
下面的组件采用项目列表并在屏幕上显示它们。此外,它需要一个对象来表示选择了哪个项目,选择并将滚动顶部设置为该项目。
// in application.hbs
{{select-item-list items=items selectedItem=selection}}
渲染时,组件将遍历给定列表并将类应用于被选择的那个列。
//in component select-item-list.hbs
<div class='item-list'>
{{#each items as |item|}}
<div class="list-item {{if item.isSelected 'selected-item'}} ">
{{item.label}}
</div>
{{/each}}
</div>
滚动发生在didRender
上,它将组件容器滚动到所选类名的元素。
//in component select-item-list.js
export default Component.extend({
didReceiveAttrs() {
this._super(...arguements);
this.items.forEach((item) => {
if(item.id ==== this.selectedItem.id) {
item.isSelected = true;
}
})
},
didRender(){
this._super(...arguments);
const scrollTarget = Math.abs(this.element.getBoundingClientRect().top - this.element.querySelector('.selected-item').getBoundingClientRect().top);
this.element.querySelector('item-list').scrollTop = scrollTarget;
}
})
使用willDestroyElement
分离和拆除组件元素
当组件检测到是时候从DOM中删除它自己时,Ember将触发willDestroyElement()
方法,允许任何拆卸逻辑被执行。
组件的拆卸可以被一些不同的条件触发。例如,用户可能跳转到不同路由,或者你的组件周围的Handlebars模块可能会更改:
//in application.hbs
{{#if falseBool}}
{{my-component}}
{{/if}}
我们使用这个钩子清理我们的时间选择器和上面的事件监听器:
//in component.js
export dafault Component.extend({
willDestoryElement() {
this.element.removeEventListener('animationend');
this.datePicker.destroy();
this._super(...arguments);
}
})
`
网友评论