评论

深入理解小程序——生命周期

如果没有像 TDesign 组件库这样深入开发小程序,可能并不会去深入钻研生命周期的细节。但在日常的业务开发当中,如果开发者能够清晰地理解各种生命周期的本质,在遇到其他问题的时候,也能比较快速地定位问

生命周期类型

不像其他框架,小程序是有页面(page)组件(Component) 两个概念,所以可以理解有两种生命周期。不过,你也可以使用组件也注册页面,然后在 lifetimes 字段里声明组件的生命周期,在 pageLifetimes 里声明页面的生命周期。

页面的生命周期有:

  • onLoad
  • onShow
  • onReady
  • onHide
  • onUnload

组件的生命周期有:

  • created
  • attached
  • ready
  • moved
  • detached
  • error

这里其实还有一个 linked 生命周期,存在父子关系时会触发

条件渲染的影响

组件可以通过 wx:ifhidden 来控制渲染的,这里对生命周期的触发也有影响。

如定义这么一个组件 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 进行隐藏,那么此时 createdattachedready 生命周期均会正常触发。如果在这些生命周期里获取子元素的尺寸,则所有值均返回 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

最后一次编辑于  2023-08-18  
点赞 2
收藏
评论

1 个评论

  • Jianbo
    Jianbo
    2023-09-03

    学习了。

    2023-09-03
    赞同
    回复
登录 后发表内容