生命周期类型
不像其他框架,小程序是有页面(page) 和组件(Component) 两个概念,所以可以理解有两种生命周期。不过,你也可以使用组件也注册页面,然后在 lifetimes
字段里声明组件的生命周期,在 pageLifetimes
里声明页面的生命周期。
页面的生命周期有:
- onLoad
- onShow
- onReady
- onHide
- onUnload
而组件的生命周期有:
- created
- attached
- ready
- moved
- detached
- error
这里其实还有一个 linked 生命周期,存在父子关系时会触发
条件渲染的影响
组件可以通过 wx:if
和 hidden
来控制渲染的,这里对生命周期的触发也有影响。
如定义这么一个组件 log
:
Component({
lifetimes: {
attached() {
console.log('log attached')
}
}
})
使用 wx:if
然后在页面中使用 wx:if
条件渲染:
<view wx:if="{{false}}">
<log />
</view>
此时不会触发 attached
,因此控制台没有输出。
使用 hidden
反而如果使用 hidden
条件渲染:
<view hidden="{{true}}">
<log />
</view>
此时反而控制台会输出 log attached
。
两者差异
其实两者的差异在于,hidden
会正常渲染 DOM,而 wx:if
则不会渲染。
如果组件的父元素使用 hidden
进行隐藏,那么此时 created
、attached
、ready
生命周期均会正常触发。如果在这些生命周期里获取子元素的尺寸,则所有值均返回 0。
如 TDesign 里面的 swipe-cell
需要计算 left 和 right 区域的大小;tabs
需要计算下划线的位置。
解决方案
比较简单的处理方式:建议用户使用 wx:if
而不是 hidden
,不过这明显是治标不治本的方案。
前文也提到了,问题的根本是没有正确地获取到元素的尺寸,因此可以在获取元素尺寸的地方做兼容处理。异常触发的条件则是 width == 0 && right == 0
知道在哪里需要兼容处理之后,需要解决的则是:如何在可以获取到正确的尺寸的时候重新获取尺寸呢?
此时可以使用 wx.createIntersectionObserver
这个 API。当 hidden = false
的时候,组件会重新出现在视图里,Observer 就会被触发,此时重新获取尺寸就能得到正确的尺寸信息了。以下是简易的封装:
const getObserver = (context, selector) => {
return new Promise((resolve, reject) => {
wx.createIntersectionObserver(context)
.relativeToViewport()
.observe(selector, (res) => {
resolve(res);
});
});
};
const getRect = function (context:, selector) {
return new Promise((resolve, reject) => {
wx.createSelectorQuery()
.in(context)
.select(selector)
.boundingClientRect((rect) => {
if (rect) {
resolve(rect);
} else {
reject(rect);
}
})
.exec();
});
};
export const getRectFinally = (context, selector) => {
return new Promise((resolve, reject) => {
getRect(context, selector).then(rect => {
if (rect.width === 0 && rect.height === 0) {
getObserver(context, selector).then(res => {
resolve(res)
}).catch(reject)
} else {
resolve(rect)
}
}).catch(reject)
})
}
父子组件的影响
当存在父子组件的时候,可能很多人根本不知道各种生命周期的触发顺序。
之前 TDesign 的 cell-group
有个错误的实现,在 linked 生命周期里获取子元素进行操作:
Component({
relations: {
'../cell/cell': {
type: 'child',
linked() {
this.updateLastChid();
},
},
},
updateLastChid() {
const items = this.$children;
items.forEach((child, index) => child.setData({ isLastChild: index === items.length - 1 }));
},
})
其实,存在父子组件的时候,生命周期是这么触发的:
- 父组件 created
- 子组件 created
- 父组件 attached
- 子组件 attached
- 父组件 linked(触发多次,次数 = 子组件数量)
- 子组件 linked
- 父组件 ready
- 子组件 ready
因此如果是这么使用 t-cell-group
:
<t-cell-group>
<t-cell title="cell1" />
<t-cell title="cell2" />
<t-cell title="cell3" />
</t-cell-group>
那么子组件 cell 的 setData
触发次数为:1 + 2 + 3 = 6 次。
但其实开发者的预期应该是 1 次,所以 updateLastChid
应该放在父组件的 ready 方法里才符合预期。
总结
以上是在小程序开发的过程中,常遇到的问题。但如果没有像 TDesign 组件库这样深入开发小程序,可能并不会去深入钻研生命周期的细节。但在日常的业务开发当中,如果开发者能够清晰地理解各种生命周期的本质,在遇到其他问题的时候,也能比较快速地定位问题的关键点。
如果能到达这样的效果,也是笔者写下这篇文章的初心。
更多内容关注:https://github.com/LeeJim
学习了。