ARTICLE · 1155057
uni-app 三类生命周期全景:应用、页面与组件
一、应用生命周期: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() // 这时候登录态才是确定的})
二、冷启动完整时序:一条主线背下来

两个细节别放过。第一,页面 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,如果清理逻辑有先后依赖(比如先停轮询、再等最后一个请求落盘),把顺序敏感的部分放前面那个里。
五、组件里写页面钩子:静默失效与三种正确姿势
组件要响应「显隐」,有三种正确的姿势,按场景选:
•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 对栈的操作不同,触发的钩子也不同,这是「返回后为什么没刷新」「为什么参数丢了」这类问题的根源:

两个实战推论。其一,navigateTo 跳详情页再返回,列表页只走 onShow,所以「编辑完返回要刷新列表」的逻辑必须挂在 onShow 上,配合标记位避免首次进入的重复请求。其二,redirectTo 会把当前页从栈里卸掉,如果当前页注册过 uni.$on 或定时器,跳转前不清理就是泄漏;登录成功后用 reLaunch 到首页是惯用做法,因为登录前的中间页(登录页、验证码页)都不该留在栈里让用户「返回」回去。
七、多端差异清单:热启动、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 时展开细讲。
这篇把生命周期的地图铺开了:应用层 onLaunch 全局一次管初始化,异步初始化要用 Promise 等待模式防竞态;页面层五个钩子按时序各管一段,onLoad 拿参数、onShow 管刷新、onReady 才能量 DOM;四种跳转方式对页面栈的操作决定了返回后刷新逻辑写在哪;组件层只有 Vue 钩子,页面钩子写进去是静默失效,用 v-if、expose、事件驱动三选一替代。加上热启动不走 onLoad、tab 页常驻、H5 刷新等价冷启动这些差异点,时序类问题基本都能定位到根因。
觉得有用的话,点赞、分享、推荐三连支持一下。