📌 本文定位: 面向同时具备 iOS、Android、鸿蒙原生开发经验,正在转向 uni-app 的资深工程师。所有技术点均基于 uni-app 官方文档 及 Vue3 官方文档整理。
一、先搞懂一个 JS 语法问题:为什么说"与 data/methods 平级的叫生命周期"?
很多原生工程师初学 uni-app 时会对这句话感到困惑。我们从 JavaScript 语言层面彻底讲清楚。
1.1 Options API 的本质是一个"配置对象"
在 Vue2/Vue3 的 Options API 中,export default {} 导出的是一个普通 JS 对象。这个对象的每个 key 都有特定语义:
javascript
export default {
// ✅ 数据属性 → Vue 会将其转为响应式
data() {
return { count: 0 }
},
// ✅ 方法属性 → Vue 会将其绑定到组件实例上
methods: {
increment() { this.count++ }
},
// ✅ 计算属性 → Vue 会缓存其返回值
computed: {
doubleCount() { return this.count * 2 }
},
// ✅ 生命周期钩子 → Vue 会在特定时机自动调用
onLoad(options) {
console.log('页面加载', options)
},
onShow() {
console.log('页面显示')
},
mounted() {
console.log('DOM 挂载完成')
}
}
1.2 Vue 内部是如何区分它们的?
Vue 在创建组件实例时,会遍历这个配置对象的所有 key,根据 key 的名称做不同的处理:
| key 名称 | Vue 内部处理方式 | 归类 |
|---|---|---|
data |
执行函数,将返回值做响应式代理(reactive()) |
数据层 |
methods |
遍历对象,将每个方法绑定 this 并挂载到实例 |
方法层 |
computed |
创建惰性求值的响应式引用(computed()) |
计算层 |
watch |
注册侦听器(watch()) |
监听层 |
onLoad / onShow / mounted 等 |
注册为生命周期回调,在对应时机由框架自动调用 | 生命周期层 |
| 其他自定义 key | 直接挂载到实例上,不做特殊处理 | 自定义属性 |
💡 核心结论: "生命周期"不是一个 JS 语言概念,而是 Vue/uni-app 框架的约定 。框架通过识别特定的 key 名称,将其从普通属性中"挑出来",注册到内部的生命周期调度系统中。这就是为什么它必须与
data、methods"平级"------因为它们都是同一个配置对象的顶层 key。
1.3 对比原生开发中的类似模式
| 平台 | 类似机制 | 说明 |
|---|---|---|
| iOS | UIViewController 的 viewDidLoad()、viewWillAppear() 等 |
系统通过方法名识别生命周期,开发者重写即可 |
| Android | Activity 的 onCreate()、onResume() 等 |
框架通过方法签名识别,自动在对应时机回调 |
| 鸿蒙 | @Component 的 aboutToAppear()、onPageShow() 等 |
ArkUI 框架通过装饰器+方法名识别 |
| Vue3 Composition API | onMounted()、onUnmounted() 等组合式函数 |
不再依赖对象 key 名,改为显式函数调用注册 |
| uni-app Options API | onLoad()、onShow() 等作为配置对象顶层 key |
框架通过 key 名识别并注册 |
二、应用生命周期详解(App.vue)
应用生命周期是全局级别 的,只能在 App.vue 中监听。它管理的是整个 App 进程从创建到销毁的全过程。
2.1 完整生命周期流程图
scss
App 启动
│
▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ onLaunch │────►│ onShow │◄───►│ onHide │
│ (仅一次) │ │(每次前台) │ │(每次后台) │
└──────────┘ └──────────┘ └──────────┘
│
用户关闭App
│
▼
┌──────────────┐
│ onExit │
│ (仅Android) │
└──────────────┘
异常分支:
├─ JS/API错误 ──► onError()
├─ 页面不存在 ──► onPageNotFound()
└─ 主题切换 ──► onThemeChange()
2.2 逐个详解 + 四端对照
onLaunch(options)
- 触发时机: App 初始化完成时触发,全局仅一次
- 参数:
options包含启动路径path、场景值scene、查询参数query等 - 典型用途: SDK 初始化、登录态检查、全局数据预加载
javascript
// App.vue
export default {
onLaunch(options) {
console.log('App Launch, 启动参数:', options);
// 初始化推送 SDK
// #ifdef APP-PLUS
plus.push.addEventListener('click', (msg) => {
console.log('推送点击:', msg);
});
// #endif
// 检查登录态
const token = uni.getStorageSync('token');
if (!token) {
uni.reLaunch({ url: '/pages/login/login' });
}
}
}
onShow(options)
- 触发时机: App 启动 或 从后台切回前台时触发
- 参数: 同
onLaunch - 典型用途: 刷新数据、恢复任务、版本检查
javascript
export default {
onShow(options) {
console.log('App Show, 场景值:', options.scene);
// 每次回到前台都检查是否有新消息
this.checkNewMessages();
}
}
onHide()
- 触发时机: App 从前台进入后台时触发
- 典型用途: 暂停任务、保存状态、释放资源
javascript
export default {
onHide() {
console.log('App Hide');
// 保存播放进度
uni.setStorageSync('music_progress', this.currentProgress);
}
}
onError(err)
- 触发时机: JS 执行错误或 API 调用失败时
- 典型用途: 全局错误上报、崩溃监控
javascript
export default {
onError(err) {
console.error('全局错误:', err);
// 上报到 Sentry / Bugly
// reportError(err);
}
}
onPageNotFound(res)
- 触发时机: 打开不存在的页面时(如分享链接指向已删除页面)
- 参数:
res.path(不存在的路径)、res.query - 典型用途: 降级跳转到首页或404页
javascript
export default {
onPageNotFound(res) {
console.warn('页面不存在:', res.path);
uni.redirectTo({ url: '/pages/index/index' });
}
}
onThemeChange(res)
- 触发时机: 系统深色/浅色模式切换时
- 参数:
res.theme('dark'|'light')
javascript
export default {
onThemeChange(res) {
console.log('主题切换:', res.theme);
// 通知全局状态更新
getApp().globalData.isDarkMode = res.theme === 'dark';
}
}
2.3 应用生命周期四端对照表(重点收藏)
| uni-app | iOS | Android | 鸿蒙 (ArkUI) | Vue3 App 级 | 说明 |
|---|---|---|---|---|---|
onLaunch |
application(_:didFinishLaunchingWithOptions:) |
Application.onCreate() |
AbilityStage.onCreate() |
app.onMount() (无直接对应) |
进程创建,全局仅一次 |
onShow |
applicationDidBecomeActive(_:) / sceneDidBecomeActive(_:) |
Activity.onResume() / ProcessLifecycleOwner ON_START |
UIAbility.onForeground() |
--- | 进入前台/可见 |
onHide |
applicationDidEnterBackground(_:) / sceneDidEnterBackground(_:) |
Activity.onPause() / ProcessLifecycleOwner ON_STOP |
UIAbility.onBackground() |
--- | 进入后台/不可见 |
onExit |
applicationWillTerminate(_:) |
Activity.onDestroy() / Application.onTerminate() |
UIAbility.onDestroy() |
app.onUnmount() |
进程销毁(不一定触发) |
onError |
NSSetUncaughtExceptionHandler |
Thread.UncaughtExceptionHandler |
AbilityStage.onError() |
app.config.errorHandler |
全局异常捕获 |
onPageNotFound |
--- | --- | --- | router.onError() |
路由兜底 |
onThemeChange |
traitCollectionDidChange(_:) |
onConfigurationChanged() |
Environment.colorScheme 变化 |
--- | 深色模式切换 |
⚠️ 关键差异提醒:
- iOS 的
applicationWillTerminate在多任务杀进程时不一定被调用 ,uni-app 的onExit同理- Android 的
onDestroy也不保证触发,不要在此做关键数据持久化- 鸿蒙的
UIAbility.onForeground/onBackground是 Ability 级别,与 uni-app 的 App 级别不完全等价- Vue3 本身没有 App 级别的 show/hide 概念,这是 uni-app 针对移动端扩展的
三、页面生命周期详解(pages/*.vue)
页面生命周期是页面级别 的,在每个 .vue 页面文件中监听。它管理的是单个页面从创建到销毁的过程。
3.1 完整页面生命周期流程图
scss
navigateTo / redirectTo / reLaunch
│
▼
┌──────────┐
│ onLoad │ ← 接收页面参数 options
└────┬─────┘
│
▼
┌──────────┐
│ onShow │ ← 每次页面显示都触发
└────┬─────┘
│
▼
┌─────────────┐
│ onReady │ ← DOM/原生渲染完成(仅首次)
└──────┬──────┘
│
用户操作/导航
│
┌───────┴───────┐
▼ ▼
┌──────────┐ ┌────────────┐
│ onHide │ │ onUnload │
│(页面隐藏) │ │(页面销毁) │
└──────────┘ └────────────┘
│ │
▼ ▼
再次 onShow 生命周期结束
3.2 逐个详解 + 示例代码
onLoad(options)
- 触发时机: 页面创建时触发,每个页面实例仅一次
- 参数:
options为页面路由参数(URL query 解析后的对象) - 典型用途: 接收参数、初始化页面数据、发起首次请求
javascript
// pages/detail/detail.vue
export default {
onLoad(options) {
// options.id = "123" (来自 /pages/detail/detail?id=123)
this.productId = options.id;
this.loadProductDetail(options.id);
},
data() {
return { productId: '', product: {} }
},
methods: {
async loadProductDetail(id) {
const res = await getProductById(id);
this.product = res.data;
}
}
}
onShow()
- 触发时机: 页面每次显示时触发(包括首次加载和从其他页面返回)
- 典型用途: 刷新列表、更新状态、埋点曝光
javascript
export default {
onShow() {
// 每次回到此页面都刷新订单列表
this.refreshOrderList();
// 埋点
uni.reportEvent('page_view', { page: 'order_list' });
}
}
onReady()
- 触发时机: 页面初次渲染完成时触发(DOM/原生视图就绪)
- 典型用途: 操作 DOM、初始化地图/视频/canvas 等需要视图就绪的组件
- 注意: 仅在首次加载时触发,
navigateBack返回不会再次触发
javascript
export default {
onReady() {
// 地图组件需要在 ready 后才能调用方法
this.mapContext = uni.createMapContext('myMap', this);
this.mapContext.moveToLocation();
}
}
onHide()
- 触发时机: 页面隐藏时触发(
navigateTo到新页面、切后台、切 Tab) - 典型用途: 暂停动画/视频、取消定时器
javascript
export default {
onHide() {
// 暂停轮播图定时器
clearInterval(this.swiperTimer);
}
}
onUnload()
- 触发时机: 页面卸载时触发(
navigateBack、redirectTo、reLaunch) - 典型用途: 清理资源、移除事件监听、取消网络请求
javascript
export default {
onUnload() {
// 清除定时器
clearInterval(this.timer);
// 移除全局事件监听
uni.$off('cartUpdated', this.onCartUpdated);
}
}
其他页面生命周期
| 函数 | 触发时机 | 用途 |
|---|---|---|
onPullDownRefresh() |
下拉刷新时 | 需 pages.json 开启 enablePullDownRefresh |
onReachBottom() |
滚动到底部时 | 分页加载更多 |
onShareAppMessage() |
点击右上角分享时 | 返回分享内容配置 |
onPageScroll(obj) |
页面滚动时 | obj.scrollTop;慎用,性能敏感 |
onResize(obj) |
窗口尺寸变化时 | 横竖屏切换、折叠屏展开 |
onTabItemTap(obj) |
点击当前 TabBar 项时 | 仅当前 Tab 页有效 |
onBackPress() |
返回键/手势/导航栏返回时 | 可拦截返回行为,返回 true 阻止默认返回 |
3.3 页面生命周期四端对照表
| uni-app | iOS | Android | 鸿蒙 (ArkUI) | Vue3 组件生命周期 | 说明 |
|---|---|---|---|---|---|
onLoad |
viewDidLoad() / init(coder:) |
onCreate() |
aboutToAppear() |
onBeforeMount |
页面创建,接收参数 |
onShow |
viewWillAppear(_:) |
onResume() |
onPageShow() |
onMounted (首次) |
页面可见 |
onReady |
viewDidAppear(_:) (首次) |
onWindowFocusChanged(true) (首次) |
onDidBuild() |
onMounted |
视图渲染完成 |
onHide |
viewWillDisappear(_:) |
onPause() |
onPageHide() |
onBeforeUnmount (部分) |
页面不可见 |
onUnload |
deinit / viewDidDisappear (最终) |
onDestroy() |
aboutToDisappear() |
onUnmounted |
页面销毁 |
onPullDownRefresh |
UIRefreshControl |
SwipeRefreshLayout |
Refresh 组件 |
--- | 下拉刷新 |
onReachBottom |
scrollViewDidScroll + offset判断 |
RecyclerView.OnScrollListener |
List.onReachEnd |
--- | 触底加载 |
onBackPress |
navigationController?.popViewController 拦截 |
onBackPressedDispatcher |
onBackPress() |
--- | 返回拦截 |
onPageScroll |
scrollViewDidScroll(_:) |
OnScrollListener |
onScroll() |
--- | ⚠️ 高频触发 |
四、Vue3 生命周期 vs uni-app 生命周期:两套体系如何共存?
这是很多开发者最容易混淆的点。uni-app 中实际上存在两套生命周期系统,它们各司其职:
4.1 双轨制对照
| 类别 | Vue3 标准生命周期 | uni-app 扩展生命周期 | 职责划分 |
|---|---|---|---|
| 组件级 | onBeforeMount / onMounted / onBeforeUpdate / onUpdated / onBeforeUnmount / onUnmounted |
--- | 管理 DOM/虚拟节点 的创建、更新、销毁 |
| 页面级 | --- | onLoad / onShow / onReady / onHide / onUnload / onPullDownRefresh / onReachBottom 等 |
管理 页面路由、导航、交互 相关逻辑 |
| 应用级 | app.onMount / app.onUnmount |
onLaunch / onShow / onHide / onError 等 |
管理 App 进程 的全局状态 |
4.2 执行顺序(关键!)
当一个页面首次加载时,两套生命周期的执行顺序如下:
scss
onLoad (uni-app) ← 最早,接收参数
│
▼
onBeforeMount (Vue3) ← 虚拟 DOM 即将挂载
│
▼
onMounted (Vue3) ← 虚拟 DOM 挂载完成
│
▼
onReady (uni-app) ← 原生视图/真实 DOM 渲染完成
│
▼
onShow (uni-app) ← 页面可见
⚠️ 重要区别:
onMounted≠onReady:onMounted表示 Vue 虚拟 DOM 挂载完成,但原生视图可能还没渲染好;onReady才表示真实视图就绪- 操作 canvas/map/video 等原生组件,必须在
onReady中 ,而非onMountedonShow在onReady之后触发,且每次页面显示都会触发
4.3 Composition API 中的写法
如果你使用 Vue3 <script setup> 语法,两套生命周期都可以直接使用:
xml
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow, onReady, onUnload } from '@dcloudio/uni-app';
const productId = ref('');
const product = ref({});
// ✅ uni-app 页面生命周期
onLoad((options) => {
productId.value = options.id;
console.log('onLoad:', options);
});
onShow(() => {
console.log('onShow');
});
onReady(() => {
console.log('onReady - 可以操作原生组件了');
});
onUnload(() => {
console.log('onUnload - 清理资源');
});
// ✅ Vue3 组件生命周期
onMounted(() => {
console.log('onMounted - 虚拟DOM已挂载');
});
onUnmounted(() => {
console.log('onUnmounted - 组件卸载');
});
</script>
📖 注意: 在
<script setup>中使用 uni-app 页面生命周期,需要从@dcloudio/uni-app导入。这些是 uni-app 对 Vue3 的组合式函数扩展。
五、JS 架构深层理解:Options API vs Composition API 的生命周期注册机制
作为资深工程师,理解框架"怎么做到的"比"怎么用"更重要。
5.1 Options API:基于对象 key 名的约定式注册
javascript
// 你写的代码
export default {
data() { return { count: 0 } },
methods: { add() { this.count++ } },
onLoad(opts) { /* ... */ },
onShow() { /* ... */ }
}
框架内部伪代码:
ini
function createComponentInstance(options) {
const instance = {};
for (const key in options) {
if (key === 'data') {
// 响应式处理
instance.data = reactive(options.data());
} else if (key === 'methods') {
// 绑定 this
Object.keys(options.methods).forEach(m => {
instance[m] = options.methods[m].bind(instance);
});
} else if (LIFECYCLE_HOOKS.includes(key)) {
// 🔑 关键:识别生命周期 key,注册到调度器
registerLifecycleHook(instance, key, options[key]);
} else {
// 其他属性直接挂载
instance[key] = options[key];
}
}
return instance;
}
const LIFECYCLE_HOOKS = [
'onLoad', 'onShow', 'onReady', 'onHide', 'onUnload',
'onPullDownRefresh', 'onReachBottom', 'onShareAppMessage',
'onPageScroll', 'onResize', 'onTabItemTap', 'onBackPress',
'mounted', 'beforeMount', 'unmounted', 'beforeUnmount'
];
5.2 Composition API:基于显式函数调用的声明式注册
javascript
// 你写的代码
import { onLoad, onShow } from '@dcloudio/uni-app';
import { onMounted } from 'vue';
onLoad((opts) => { /* ... */ });
onShow(() => { /* ... */ });
onMounted(() => { /* ... */ });
框架内部伪代码:
ini
// @dcloudio/uni-app 中的实现
let currentInstance = null;
export function onLoad(callback) {
// 获取当前正在创建的组件实例
const instance = getCurrentInstance();
// 将回调注册到实例的生命周期队列中
instance.lifecycleHooks.onLoad = callback;
}
export function onShow(callback) {
const instance = getCurrentInstance();
instance.lifecycleHooks.onShow = callback;
}
5.3 两种方式的本质区别
| 维度 | Options API | Composition API |
|---|---|---|
| 注册方式 | 对象 key 名约定 | 显式函数调用 |
| 识别机制 | 框架遍历 key 名匹配 | 开发者主动调用注册函数 |
| 灵活性 | 固定结构,不易拆分 | 可按逻辑关注点自由组织 |
| TypeScript 支持 | 较弱(需额外类型声明) | 天然支持(函数有明确类型) |
| 代码复用 | Mixin(命名冲突风险) | Composables(清晰无冲突) |
| 推荐场景 | 简单页面、快速原型 | 复杂业务、团队协作、长期维护 |
六、实战最佳实践与避坑指南
6.1 数据初始化放哪里?
| 场景 | 推荐位置 | 原因 |
|---|---|---|
| 依赖 URL 参数的数据 | onLoad |
只有这里能拿到 options |
| 不依赖参数的静态数据 | data() / ref() |
声明即初始化 |
| 每次进入都需要刷新的数据 | onShow |
返回时也能刷新 |
| 需要 DOM 就绪的数据 | onReady |
如地图中心点、canvas 尺寸 |
| 全局共享数据 | App.globalData / Pinia |
跨页面共享 |
6.2 常见坑点速查
| 坑点 | 现象 | 解决方案 |
|---|---|---|
onLoad 中跳转白屏 |
App 未完全初始化 | 改用 onShow 或 setTimeout 延迟跳转 |
onMounted 中操作 map 报错 |
原生视图未就绪 | 改用 onReady |
onShow 中拿不到最新参数 |
onShow 无 options 参数 |
在 onLoad 中存到 data,onShow 中读取 |
onPageScroll 卡顿 |
高频触发导致通信开销 | 用 CSS position: sticky 替代,或节流处理 |
onUnload 不触发 |
使用了 switchTab 而非 navigateBack |
Tab 页用 onHide 代替 |
globalData 修改后页面不更新 |
非响应式 | 在 onShow 中重新赋值,或改用 Pinia |
H5 端 onLoad 参数丢失 |
浏览器刷新导致 | H5 端建议用 Vue Router 的 route.query 辅助 |
6.3 生命周期选择决策树
scss
需要在某个时机执行代码?
│
├─ 与 App 进程相关?
│ ├─ 初始化/SDK ──► onLaunch
│ ├─ 前后台切换 ──► onShow / onHide
│ └─ 全局错误 ──► onError
│
├─ 与页面导航相关?
│ ├─ 接收参数 ──► onLoad
│ ├─ 每次显示刷新 ──► onShow
│ ├─ 返回拦截 ──► onBackPress
│ └─ 页面销毁清理 ──► onUnload
│
├─ 与视图渲染相关?
│ ├─ 操作原生组件 ──► onReady
│ ├─ DOM 操作(Vue) ──► onMounted
│ └─ 组件卸载 ──► onUnmounted
│
└─ 与用户交互相关?
├─ 下拉刷新 ──► onPullDownRefresh
├─ 触底加载 ──► onReachBottom
├─ 滚动监听 ──► onPageScroll (⚠️慎用)
└─ 分享 ──► onShareAppMessage
七、总结:四端生命周期全景对照表
| uni-app | iOS | Android | 鸿蒙 | Vue3 | 层级 |
|---|---|---|---|---|---|
onLaunch |
didFinishLaunching |
Application.onCreate |
AbilityStage.onCreate |
--- | App |
onShow |
didBecomeActive |
onResume |
onForeground |
--- | App |
onHide |
didEnterBackground |
onPause |
onBackground |
--- | App |
onExit |
willTerminate |
onDestroy |
onDestroy |
app.onUnmount |
App |
onLoad |
viewDidLoad |
onCreate |
aboutToAppear |
onBeforeMount |
Page |
onShow (页面) |
viewWillAppear |
onResume |
onPageShow |
onMounted |
Page |
onReady |
viewDidAppear |
onWindowFocusChanged |
onDidBuild |
onMounted |
Page |
onHide (页面) |
viewWillDisappear |
onPause |
onPageHide |
--- | Page |
onUnload |
deinit |
onDestroy |
aboutToDisappear |
onUnmounted |
Page |
💡 给多端原生工程师的一句话: uni-app 的生命周期设计是对 iOS/Android/鸿蒙三者最大公约数的抽象。
onLoad ≈ viewDidLoad ≈ onCreate ≈ aboutToAppear,onShow ≈ viewWillAppear ≈ onResume ≈ onPageShow。掌握了这个映射关系,你就能在四种技术栈之间自如切换。框架在变,生命周期的本质思想从未改变。
📚 参考资料: