夜雨聆风学习资料网

ARTICLE · 1155057

uni-app 三类生命周期全景:应用、页面与组件

uni-app 三类生命周期全景:应用、页面与组件
生命周期是排查「代码没执行」「执行了两次」「时机不对」这类问题的底层地图。uni-app 的生命周期分应用、页面、组件三层,Vue3 写法下又有页面专属钩子和组合式 API 的交界地带。这篇把三层的完整时序、五个页面钩子各自的职责、四种跳转方式触发的钩子差异、Vue3 setup 里的正确写法、多端差异,以及三个我真实踩过的时序坑,一次铺成一张全景图。

一、应用生命周期:App.vue 里的全局回调

// App.vue(Vue3 setup 写法)<script setup>import { onLaunch, onShow, onHide, onError, onPageNotFound } from '@dcloudio/uni-app'onLaunch(() => {  // 全程只执行一次:应用初始化、读缓存恢复登录态})onShow(() => {  // 应用切前台:检查版本更新、刷新角标})onHide(() => {  // 应用切后台:上报埋点、暂停轮询})onError((err) => {  // 全局 JS 错误兜底:格式化后上报到自己的监控接口})onPageNotFound((res) => {  // 小程序端:路径写错或下线页面被收藏,兜底跳首页  uni.reLaunch({ url: '/pages/index/index' })})</script>

应用级回调在全局只走一次(onLaunch)或随前后台切换走(onShow/onHide),适合放「整个应用只该有一份」的逻辑:登录态恢复、全局错误上报、更新检查。onError 是全局兜底的好位置,把异常格式化后上报,配合 sourcemap 才能定位压缩后的报错;onPageNotFound 只在小程序端有实际意义,用户收藏了已下线的页面时,至少把人领回首页而不是白屏。把业务请求塞进 onLaunch 是常见误用,页面该自己管自己的数据。

onLaunch 的竞态:最容易被忽视的一个坑

onLaunch 里的逻辑经常是异步的,比如从 storage 读 token、再发一个 /me 接口恢复登录态。但首页不会等你:小程序的首页 onLoad 几乎和 onLaunch 同时开始执行。结果就是首页的第一批请求在登录态恢复完成之前就发出去了,轻则带不上 token 被打回 401,重则被全局拦截器误判为「未登录」直接踢到登录页。

解法是把初始化包装成一个 Promise,挂到全局,页面在发第一批请求前先等它:

// utils/appInit.jslet initPromise = nullexport function ensureAppReady() {  if (!initPromise) {    initPromise = new Promise((resolve) => {      const token = uni.getStorageSync('token')      if (!token) return resolve()      uni.request({        url: 'https://api.example.com/me',        header: { Authorization: 'Bearer ' + token },        success: (res) => resolve(res.data),        fail: () => resolve(null),      })    })  }  return initPromise}// 页面里onLoad(async () => {  await ensureAppReady()  fetchList() // 这时候登录态才是确定的})
我踩过的坑:一开始把恢复登录态写在 App.vue 的 onLaunch 里,然后奇怪首页每次冷启动都偶发跳登录页——真机上偶现、模拟器不复现,因为时序快慢不同。改成 ensureAppReady 的等待模式后才彻底稳定。凡是在 onLaunch 里做异步初始化的项目,都值得自查一遍这个竞态。

二、冷启动完整时序:一条主线背下来

背下这条链,执行顺序类的问题都能对号入座

两个细节别放过。第一,页面 onShow 触发在组件 mounted 之前,所以组件 mounted 里改的变量,页面 onShow 里第一次读到的可能还是旧值;依赖渲染结果的逻辑放 onReady 或 watch 更稳。第二,onLoad 比 onShow 先走,但两者之间没有等待关系,如果 onShow 里依赖 onLoad 设置的状态,直接写在 onLoad 里反而更清晰。

三、五个页面钩子各自的职责边界

页面层最常用的是五个钩子,各自能干的事和不能干的事分得非常清楚:

export default {  onLoad(options) {    // 只执行一次。options 是页面参数(字符串,注意 decode)    // 此刻拿得到参数,但拿不到节点尺寸    this.id = options.id  },  onShow() {    // 每次页面可见都执行:首次进入、从别的页面返回、tab 切回    // 适合刷新「可能已经变了」的数据  },  onReady() {    // 只执行一次,页面首次渲染完成    // 量 DOM、初始化图表 canvas、createSelectorQuery 放这里  },  onHide() {    // 页面不可见但未销毁:进新页面、切后台    // 暂停计时器、暂停视频播放  },  onUnload() {    // 页面销毁:清理定时器、uni.$off 解绑事件    // 忘了清理是内存泄漏和小程序页面栈报警的常见来源  },}

三个高频区别单独说。onLoad 只执行一次、参数里带启动参数,适合初始化;onShow 每次进页面都执行(包括从别的页面返回),适合刷新可能变化的数据;onReady 表示页面首次渲染完成,量 DOM、初始化图表放这里,onLoad 里去量只会量个寂寞。

还有两个容易漏掉的细节。一是 onLoad 拿到的 options 值都是字符串,布尔值和数字需要自己转,而且部分端会对参数值做 encodeURIComponent,特殊字符要先 decodeURIComponent 再用;二是 onUnload 里一定要清理的东西不止 setInterval,还有 uni.$on 注册的全局事件监听,页面销毁了监听还在,下一次同事件触发就会打到已经销毁的页面上,报错或者数据错乱。

四、Vue3 setup 里的页面钩子:从 @dcloudio/uni-app 导入

Vue3 组合式 API 下,页面专属钩子不能写在页面组件的 onMounted 里替代,要从官方包导入:

<script setup>import { onLoad, onShow, onReady, onUnload } from '@dcloudio/uni-app'import { onMounted, onUnmounted, ref } from 'vue'onLoad((options) => { /* 启动参数 */ })onShow(() => { /* 每次可见 */ })onReady(() => { /* DOM 就绪,可测量 */ })onUnload(() => { /* 页面卸载,清理定时器 */ })onMounted(() => { /* 组件级挂载,页面和普通组件都有 */ })</script>

两套钩子的分工要分清:onLoad/onShow/onReady 是页面概念(只有页面有),onMounted/onUnmounted 是 Vue 组件概念(页面和普通组件都有)。页面卸载时 onUnload 和 onUnmounted 都会触发,清理资源挂哪个都行。执行顺序上 onUnload 先于 onUnmounted,如果清理逻辑有先后依赖(比如先停轮询、再等最后一个请求落盘),把顺序敏感的部分放前面那个里。

五、组件里写页面钩子:静默失效与三种正确姿势

我踩过的坑:把 onShow 写进了一个业务组件,指望每次弹层打开时刷新数据。onShow 只认页面,组件里的它永远不会执行,而且不报错、没有警告,这类「静默失效」最耗排查时间。同理,普通组件里写 onLoad、onReady 也全都不生效。

组件要响应「显隐」,有三种正确的姿势,按场景选:

•v-if 控制:弹层类的组件用 v-if 挂载卸载,组件内部的 onMounted/onUnmounted 天然跟着走,初始化和清理都不用额外写

•expose 方法:页面在 onShow 里调组件暴露出来的 refresh(),把「页面可见」这个信号显式传给组件,调用关系一目了然

•事件驱动:多个组件都要响应时用 uni.$emit 广播,组件里 uni.$on 接,记得在 onUnmounted 里 $off,不然会重复注册

顺带一提 Vue3 的一个语法细节:页面钩子从 @dcloudio/uni-app 导入后,写在页面的 <script setup> 顶层就能自动关联到当前页面;但如果写在了自定义 hook(组合式函数)里被页面调用,同样生效,前提是这个 hook 在页面 setup 的同步执行链里被调用过。把它塞进异步回调或普通 js 文件里再调用,注册不上。

六、四种跳转方式的钩子差异:谁 onLoad、谁 onUnload

页面栈是理解跳转钩子的钥匙。uni-app 维护一个最多十层的页面栈,不同跳转 API 对栈的操作不同,触发的钩子也不同,这是「返回后为什么没刷新」「为什么参数丢了」这类问题的根源:

选跳转 API 前先想清楚:栈要留还是要清

两个实战推论。其一,navigateTo 跳详情页再返回,列表页只走 onShow,所以「编辑完返回要刷新列表」的逻辑必须挂在 onShow 上,配合标记位避免首次进入的重复请求。其二,redirectTo 会把当前页从栈里卸掉,如果当前页注册过 uni.$on 或定时器,跳转前不清理就是泄漏;登录成功后用 reLaunch 到首页是惯用做法,因为登录前的中间页(登录页、验证码页)都不该留在栈里让用户「返回」回去。

页面栈上限十层:连续 navigateTo 十次后再跳会静默失败。多层级的流程(比如一步步填表)中途记得用 redirectTo 顶替旧页,既控制栈深,返回逻辑也符合用户直觉。

七、多端差异清单:热启动、H5 刷新与 App 的特殊性

•小程序热启动:冷启动走一遍完整链路;切到后台再切回(热启动)只走 App onShow 和栈顶页 onShow,onLoad 不会再来。所以「切回来要刷新」的逻辑只能放 onShow,放 onLoad 热启动下永远不执行

•H5 端:刷新页面等价于冷启动,onLaunch 到 onReady 全走一遍;但浏览器 tab 之间的切换不触发 uni-app 的页面 onShow,要监听浏览器级 visibilitychange 才能感知,两端语义在这点上是错位的

•App 端:应用切前后台会触发 App.vue 的 onShow/onHide,和页面切换的页面级 onShow/onHide 是两回事;App 的版本更新不用小程序的 getUpdateManager(那是小程序专属 API),走整包更新或热更新方案,条件编译区分开写

•下拉刷新等扩展钩子:onPullDownRefresh、onReachBottom、onShareAppMessage 都属于页面钩子家族,但需要在 pages.json 对应页面里开启配置才生效,钩子写了配置没开,同样是静默失效

这张清单的共同规律是:钩子没生效时,先查「它是不是写在了组件里」「配置有没有开」「这个端有没有这个能力」,三个方向排查完,绝大多数静默失效都能定位。

八、三个时序翻车现场:对号入座排查

现场一:偶发跳登录页。冷启动时首页请求偶发 401,真机高发、开发者工具不复现。根因是第一节的竞态:onLaunch 里异步恢复登录态,首页请求没等它。解法是 ensureAppReady 的等待模式,页面发请求前先 await。

现场二:onLoad 里量不到 DOM。用 createSelectorQuery 想拿列表容器高度做吸顶,在 onLoad 里查出来永远是 null。根因是 onLoad 时页面还没渲染。查询类操作全部挪到 onReady,问题消失。同理,canvas 图表初始化放 onLoad 里也会白屏或报节点不存在。

现场三:返回列表页重复请求两次。在 onShow 里直接刷列表,首次进入时 onLoad 和 onShow 各触发了一次数据加载,接口被打了两遍。解法是标记位:onLoad 置 false,首次 onShow 请求完置 true,后续 onShow 只在特定条件(比如从编辑页返回)才刷。这个姿势后面聊 onShow 时展开细讲。

排查口诀:代码没执行,先查「写错层」(组件里写了页面钩子)和「没配置」(pages.json 没开);执行了两次,先查 onShow 与 onLoad 的叠加、再查组件重复挂载;执行时机不对,对照冷启动时序图,确认你要的东西(参数、DOM、登录态)在哪个钩子里才就绪。
· · ·

这篇把生命周期的地图铺开了:应用层 onLaunch 全局一次管初始化,异步初始化要用 Promise 等待模式防竞态;页面层五个钩子按时序各管一段,onLoad 拿参数、onShow 管刷新、onReady 才能量 DOM;四种跳转方式对页面栈的操作决定了返回后刷新逻辑写在哪;组件层只有 Vue 钩子,页面钩子写进去是静默失效,用 v-if、expose、事件驱动三选一替代。加上热启动不走 onLoad、tab 页常驻、H5 刷新等价冷启动这些差异点,时序类问题基本都能定位到根因。

觉得有用的话,点赞、分享、推荐三连支持一下。

相关学习资料