生命周期类型
不像其他框架,小程序是有页面(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

学习了。