3. 核心概念与生命周期:App/Page/Component生命周期、onLaunch与onLoad的区别、页面栈与路由管理、我踩过的生命周期坑
说实话,刚接触小程序那会儿,我被生命周期搞得挺头疼的。尤其是多端兼容的时候,同一个生命周期在不同平台上的表现居然不一样。今天我就把这些核心概念掰开揉碎了讲,顺便分享几个我踩过的坑。
3.1 App/Page/Component 生命周期全景
小程序的三个核心角色——App、Page、Component,各自有一套生命周期。说白了,就是程序从生到死会经历哪些阶段。
App 生命周期
App 是整个小程序的根。它只有两个生命周期:
- onLaunch:小程序初始化完成时触发,全局只执行一次
- onShow:小程序启动或从后台切到前台时触发
- onHide:小程序从前台切到后台时触发
我个人的习惯:把全局的登录态校验、版本更新检查放在 onLaunch 里。但要注意,onLaunch 里如果做异步操作,页面可能已经先渲染了。
Page 生命周期
页面生命周期就丰富多了。我列个表格,大家看得更清楚:
| 生命周期 | 触发时机 | 常见用途 |
|---|---|---|
| onLoad | 页面加载时触发 | 接收参数、初始化数据 |
| onShow | 页面显示/切入前台时 | 刷新数据、恢复状态 |
| onReady | 页面初次渲染完成 | 获取节点信息、开始动画 |
| onHide | 页面隐藏/切入后台 | 暂停定时器、保存草稿 |
| onUnload | 页面卸载时 | 清理资源、取消订阅 |
Component 生命周期
组件生命周期和页面类似,但多了几个特有的:
- created:组件实例刚被创建,此时还不能调用 setData
- attached:组件被挂载到页面节点树,可以开始初始化
- ready:组件布局完成,可以获取节点信息
- detached:组件被移除,适合做清理工作
一个小技巧:在 created 里做数据初始化,在 attached 里做 DOM 相关的操作。这样能避免一些奇怪的渲染问题。
3.2 onLaunch 与 onLoad 的区别
这个问题面试经常问,也是新手最容易混淆的。我直接说结论:
- onLaunch 是 App 级别的,整个小程序生命周期只执行一次
- onLoad 是 Page 级别的,每次页面加载都会执行
但这里有个坑——冷启动 vs 热启动。冷启动时,onLaunch 先执行,然后才轮到页面的 onLoad。热启动时,onLaunch 不会执行,但 onLoad 可能会再次触发(取决于页面是否被销毁)。
我曾经踩过的坑:在 onLaunch 里获取用户信息,然后存到全局。结果页面 onLoad 时直接拿全局数据,发现是空的。为什么?因为 onLaunch 里的异步请求还没回来,页面就已经 onLoad 了。解决方案:用回调或者 Promise 来保证顺序。
// 错误示范
// app.js
onLaunch() {
wx.getUserInfo({
success: (res) => {
this.globalData.userInfo = res.userInfo
}
})
}
// pages/index.js
onLoad() {
// 此时 userInfo 可能还是 undefined
console.log(getApp().globalData.userInfo)
}
// 正确做法:用回调
// app.js
onLaunch() {
wx.getUserInfo({
success: (res) => {
this.globalData.userInfo = res.userInfo
// 通知等待的页面
if (this.userInfoReadyCallback) {
this.userInfoReadyCallback(res.userInfo)
}
}
})
}
// pages/index.js
onLoad() {
const app = getApp()
if (app.globalData.userInfo) {
console.log(app.globalData.userInfo)
} else {
app.userInfoReadyCallback = (userInfo) => {
console.log(userInfo)
}
}
}
3.3 页面栈与路由管理
小程序的路由管理,说白了就是管理页面栈。页面栈最多 10 层,超过就会出问题。我见过有人把页面栈撑爆了,然后页面白屏——嗯,那场面挺尴尬的。
路由跳转方式
- wx.navigateTo:保留当前页面,跳转到新页面(页面栈 +1)
- wx.redirectTo:关闭当前页面,跳转到新页面(页面栈不变)
- wx.switchTab:跳转到 tabBar 页面,关闭所有非 tabBar 页面
- wx.navigateBack:返回上一页或多级页面
- wx.reLaunch:关闭所有页面,打开新页面
我建议:能用 navigateTo 就别用 redirectTo,除非你确定用户不需要返回。为什么?因为用户习惯用左上角的返回按钮,你把它关了,用户会困惑。
页面栈的常见问题
我在项目中遇到过一个问题:从 A 页面跳转到 B 页面,B 页面又跳转到 C 页面。在 C 页面做完操作后,想直接返回到 A 页面。怎么做?
// 方法一:用 getCurrentPages 获取页面栈
const pages = getCurrentPages()
const prevPage = pages[pages.length - 3] // 上上级页面
wx.navigateBack({
delta: 2
})
// 方法二:用 reLaunch 重新打开
wx.reLaunch({
url: '/pages/A/A'
})
方法一适合保留页面状态,方法二适合重新初始化。具体用哪个,看业务需求。
3.4 我踩过的生命周期坑
这部分我专门整理了一下,都是真金白银换来的教训。
坑一:onShow 里重复请求
页面从后台切回前台时,onShow 会再次触发。如果你在 onShow 里做了网络请求,用户频繁切换页面就会导致重复请求。我当时的解决方案是加一个防抖:
let lastRequestTime = 0
onShow() {
const now = Date.now()
if (now - lastRequestTime < 1000) return // 1秒内不重复请求
lastRequestTime = now
this.fetchData()
}
坑二:组件 detached 后还在执行定时器
这个坑我印象特别深。有个组件里开了个定时器轮询数据,结果组件被销毁了,定时器还在跑。不仅浪费性能,还可能导致内存泄漏。后来我养成了习惯:在 detached 里清理所有定时器和订阅。
Component({
lifetimes: {
attached() {
this.timer = setInterval(() => {
this.updateData()
}, 5000)
},
detached() {
clearInterval(this.timer)
// 如果有事件订阅,也要取消
// this.eventBus.off('someEvent', this.handler)
}
}
})
坑三:多端兼容时生命周期差异
微信小程序和支付宝小程序的组件生命周期名称不一样。微信用 lifetimes,支付宝用 didMount。我刚开始做多端适配时,直接复制代码过去,结果组件不渲染。后来统一封装了一层生命周期处理。
一个小建议:如果你用 Taro 或 uni-app 这类框架,它们已经帮你抹平了生命周期差异。但如果你自己写原生,一定要在文档里标注清楚各平台的差异。
坑四:onLoad 参数丢失
有一次用户反馈,从分享卡片进入小程序,页面参数丢了。排查后发现,分享路径里的参数被编码了,onLoad 里拿到的 options 是空的。解决方案:在分享时对参数做 encodeURIComponent,在 onLoad 里做 decodeURIComponent。
// 分享时
onShareAppMessage() {
return {
path: `/pages/detail/detail?id=${encodeURIComponent(this.data.id)}`
}
}
// 接收时
onLoad(options) {
const id = decodeURIComponent(options.id)
this.setData({ id })
}
嗯,生命周期这块其实不难,但细节特别多。你想想看,一个页面从打开到关闭,中间可能经历冷启动、热启动、切后台、切前台、页面销毁……每个环节都可能出问题。我的建议是:把生命周期当成一张地图,搞清楚每个阶段能做什么、不能做什么。这样踩坑的概率会小很多。