微信小程序启动顺序
- 启动顺序
- [App.onLaunch 【异步坑】](#App.onLaunch 【异步坑】)
启动顺序
摘要 :本文梳理了微信小程序的启动顺序,区分冷启动与热启动两种场景:冷启动会完整执行
App.onLaunch → App.onShow → Page.onLoad → Page.onShow → Page.onReady,而热启动仅触发onShow。同时重点剖析了App.onLaunch中的异步坑------在onLaunch里获取 token/openId 时,页面onLoad往往先于异步请求完成执行,导致数据取不到。最后给出 Promise、回调/事件总线、页面内判断三种解决方案,并附完整代码示例。
如果用户已经打开过小程序,只是切到了后台(如按了 Home 键)又切回来:
不会再次执行 onLaunch 和页面的 onLoad(页面实例还在内存中)。
执行顺序变为:
App.onShow(应用重新展示)
Page.onShow(当前页面重新展示)
冷启动时序图
冷启动指用户首次打开小程序,或小程序被销毁后重新打开,此时会完整执行 App 与 Page 的生命周期:
页面实例 App 实例 用户 页面实例 App 实例 用户 #mermaid-svg-jTFMrtL5tjNzIBnF{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jTFMrtL5tjNzIBnF .error-icon{fill:#552222;}#mermaid-svg-jTFMrtL5tjNzIBnF .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jTFMrtL5tjNzIBnF .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jTFMrtL5tjNzIBnF .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jTFMrtL5tjNzIBnF .marker.cross{stroke:#333333;}#mermaid-svg-jTFMrtL5tjNzIBnF svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jTFMrtL5tjNzIBnF p{margin:0;}#mermaid-svg-jTFMrtL5tjNzIBnF .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTFMrtL5tjNzIBnF text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jTFMrtL5tjNzIBnF .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-jTFMrtL5tjNzIBnF .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-jTFMrtL5tjNzIBnF #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-jTFMrtL5tjNzIBnF .sequenceNumber{fill:white;}#mermaid-svg-jTFMrtL5tjNzIBnF #sequencenumber{fill:#333;}#mermaid-svg-jTFMrtL5tjNzIBnF #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-jTFMrtL5tjNzIBnF .messageText{fill:#333;stroke:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTFMrtL5tjNzIBnF .labelText,#mermaid-svg-jTFMrtL5tjNzIBnF .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .loopText,#mermaid-svg-jTFMrtL5tjNzIBnF .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-jTFMrtL5tjNzIBnF .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-jTFMrtL5tjNzIBnF .noteText,#mermaid-svg-jTFMrtL5tjNzIBnF .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-jTFMrtL5tjNzIBnF .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTFMrtL5tjNzIBnF .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTFMrtL5tjNzIBnF .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-jTFMrtL5tjNzIBnF .actorPopupMenu{position:absolute;}#mermaid-svg-jTFMrtL5tjNzIBnF .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-jTFMrtL5tjNzIBnF .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-jTFMrtL5tjNzIBnF .actor-man circle,#mermaid-svg-jTFMrtL5tjNzIBnF line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-jTFMrtL5tjNzIBnF :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 遇到 await 挂起(不阻塞后续) (某个时刻)await 请求完成 冷启动(首次打开) onLaunch 开始执行 onShow(应用展示) 页面开始加载 onLoad onShow onReady 继续执行 onLaunch 后续代码
热启动时序图
热启动指小程序从后台切回前台,页面实例仍驻留内存,不会重新执行 onLaunch 与 onLoad:
页面实例 App 实例 用户 页面实例 App 实例 用户 #mermaid-svg-LF2Etlwf6A4A3Z8g{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LF2Etlwf6A4A3Z8g .error-icon{fill:#552222;}#mermaid-svg-LF2Etlwf6A4A3Z8g .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LF2Etlwf6A4A3Z8g .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LF2Etlwf6A4A3Z8g .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LF2Etlwf6A4A3Z8g .marker.cross{stroke:#333333;}#mermaid-svg-LF2Etlwf6A4A3Z8g svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LF2Etlwf6A4A3Z8g p{margin:0;}#mermaid-svg-LF2Etlwf6A4A3Z8g .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LF2Etlwf6A4A3Z8g text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LF2Etlwf6A4A3Z8g .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LF2Etlwf6A4A3Z8g .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LF2Etlwf6A4A3Z8g #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LF2Etlwf6A4A3Z8g .sequenceNumber{fill:white;}#mermaid-svg-LF2Etlwf6A4A3Z8g #sequencenumber{fill:#333;}#mermaid-svg-LF2Etlwf6A4A3Z8g #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LF2Etlwf6A4A3Z8g .messageText{fill:#333;stroke:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LF2Etlwf6A4A3Z8g .labelText,#mermaid-svg-LF2Etlwf6A4A3Z8g .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .loopText,#mermaid-svg-LF2Etlwf6A4A3Z8g .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LF2Etlwf6A4A3Z8g .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LF2Etlwf6A4A3Z8g .noteText,#mermaid-svg-LF2Etlwf6A4A3Z8g .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LF2Etlwf6A4A3Z8g .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LF2Etlwf6A4A3Z8g .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LF2Etlwf6A4A3Z8g .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LF2Etlwf6A4A3Z8g .actorPopupMenu{position:absolute;}#mermaid-svg-LF2Etlwf6A4A3Z8g .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LF2Etlwf6A4A3Z8g .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LF2Etlwf6A4A3Z8g .actor-man circle,#mermaid-svg-LF2Etlwf6A4A3Z8g line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LF2Etlwf6A4A3Z8g :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 不执行 onLaunch 与 onLoad 热启动(后台切回前台) onShow(应用重新展示) onShow(当前页面重新展示)
App.onLaunch 【异步坑】
App.onLaunch 开始执行(遇到 await 挂起,但不阻塞)→ App.onShow 执行 → 页面开始加载:Page.onLoad 执行 → Page.onShow 执行 →(某个时刻)onLaunch 里的 await 请求完成,继续执行后续代码。
如果你在 onLaunch 里获取 token 或 openId,然后在 pages/index.ts 的 onLoad 里立刻去用这个 token,大概率是取不到的,因为 onLoad 跑得比异步请求快。
解决方案
如果页面依赖 onLaunch 的异步数据,通常有以下几种处理方式:
方案一( Promise 方案)
在 App 中定义一个 loginReady 的 Promise,页面 onLoad 时 await 这个 Promise。
App.js:
js
// app.js
App({
onLaunch() {
// 1. onLaunch 开始执行,创建 loginReady Promise
// 注意:Promise 的 executor 是同步执行的,
// 所以 loginReady 在 onLaunch 返回前就已经被赋值
this.loginReady = new Promise((resolve, reject) => {
// 2. 发起异步请求(如 wx.login 换取 code,再请求后端拿 token/openId)
wx.login({
success: (res) => {
// 3. 模拟请求后端换取 openId
setTimeout(() => {
this.globalData.openId = 'mock_open_id_' + res.code;
// 4. 异步完成,resolve 通知所有等待方
resolve(this.globalData.openId);
}, 1000);
},
fail: (err) => {
// 失败也要 resolve/reject,避免页面永久挂起
reject(err);
}
});
});
},
globalData: {
openId: ''
}
});
pages/index/index.js:
js
// pages/index/index.js
const app = getApp();
Page({
async onLoad() {
// 1. onLoad 执行时,onLaunch 可能还没完成异步请求
// 直接读 globalData.openId 大概率是空字符串
console.log('onLoad 立即读取 openId:', app.globalData.openId); // 输出:''
// 2. 正确做法:await loginReady,等待 onLaunch 的异步完成
try {
const openId = await app.loginReady;
// 3. 到这里才拿到数据,此时 onLaunch 的异步已 resolve
console.log('await 后读取 openId:', openId);
this.setData({ openId });
} catch (err) {
console.error('获取 openId 失败:', err);
}
}
});
执行顺序说明:onLaunch 先执行并创建 loginReady → onLoad 执行并 await loginReady(挂起,不阻塞页面渲染)→ 异步请求完成后 resolve → onLoad 中 await 之后的代码继续执行。
方案二 回调/事件总线
在 onLaunch 异步完成后,通过 getApp().globalData 设值,并触发回调通知页面。
// app.ts
App({
// 全局数据
globalData: {
token: null as string | null,
},
// 存放等待登录完成的页面回调函数(事件总线队列)
loginReadyCallbacks: [] as Array<(token: string) => void>,
onLaunch() {
console.log('[App] onLaunch 开始,发起异步登录');
this.asyncLogin();
},
onShow() {
console.log('[App] onShow');
},
/**
* 模拟异步登录请求
*/
asyncLogin() {
// 模拟 wx.login + wx.request 的异步过程
setTimeout(() => {
const mockToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxxxx';
console.log('[App] 异步登录完成,获取到 token');
// 1. 存入全局数据
this.globalData.token = mockToken;
// 2. 通知所有订阅者(触发回调队列)
this.loginReadyCallbacks.forEach((callback) => {
try {
callback(mockToken);
} catch (error) {
console.error('[App] 执行回调时出错:', error);
}
});
// 3. 清空队列(一次性事件,避免重复通知)
this.loginReadyCallbacks = [];
}, 2000); // 模拟 2 秒的网络延迟
},
/**
* 供页面调用的"订阅"方法
* @param callback 页面传入的回调函数
*/
addLoginReadyCallback(callback: (token: string) => void) {
// 情况 A:如果 token 已经拿到了(比如热启动),直接同步执行回调
if (this.globalData.token) {
console.log('[App] token 已存在,直接执行回调');
callback(this.globalData.token);
return;
}
// 情况 B:token 还没拿到,加入队列等待
console.log('[App] token 未就绪,页面回调已加入等待队列');
this.loginReadyCallbacks.push(callback);
},
removeLoginReadyCallback(callback: (token: string) => void) {
const index = this.loginReadyCallbacks.indexOf(callback);
if (index > -1) {
this.loginReadyCallbacks.splice(index, 1);
}
}
});
// pages/index.ts
Page({
data: {
token: '',
pageStatus: '正在等待 token...',
},
onLoad() {
console.log('[Page] onLoad');
const app = getApp();
// 订阅 App 的登录完成事件
app.addLoginReadyCallback((token) => {
console.log('[Page] 收到 token 通知,开始更新页面');
this.setData({
token: token,
pageStatus: 'Token 已就绪,页面可以发起业务请求!',
});
// 在这里你可以放心地调用需要 token 的接口
// this.fetchUserInfo(token);
});
},
onShow() {
console.log('[Page] onShow');
},
onUnload() {
// 页面销毁时,取消订阅,防止内存泄漏和 setData 警告
if (this.loginCallback) {
getApp().removeLoginReadyCallback(this.loginCallback);
}
},
});
说明:
- 核心机制:利用数组存储回调函数,异步完成后遍历调用。
- 优势:逻辑清晰,页面无需关心 App 的生命周期,只需"订阅"数据。
- 适用场景:登录态下发、地理位置获取、系统信息初始化等需要在 App 启动阶段完成并通知多个页面的异步任务。
方案三 页面内判断
页面 onShow 时检查全局数据是否就绪,未就绪则显示 loading 或重新获取。
将获取 token 的逻辑抽离成一个公共方法,并做了简单的"防止重复请求"处理。
// app.ts
App({
globalData: {
token: null as string | null,
},
// 标记是否正在请求中,防止页面和App同时调起多个请求
isFetchingToken: false,
onLaunch() {
console.log('[App] onLaunch - 应用启动,开始异步获取 token');
// App 启动时就去获取
this.fetchToken();
},
onShow() {
console.log('[App] onShow');
},
/**
* 获取 Token 的公共方法(返回 Promise)
*/
fetchToken(): Promise<string> {
return new Promise((resolve) => {
// 如果已经在请求中,这里简单模拟:直接等一会儿再检查 globalData
// (实际项目中可以用一个全局 Promise 单例来避免并发)
if (this.isFetchingToken) {
console.log('[App] 正在请求中,请稍候...');
// 简单轮询等待(仅作演示,生产环境建议用 Promise 单例)
const timer = setInterval(() => {
if (this.globalData.token) {
clearInterval(timer);
resolve(this.globalData.token);
}
}, 300);
return;
}
this.isFetchingToken = true;
// 模拟网络请求(比如调用 wx.login 和后端接口)
setTimeout(() => {
const mockToken = 'mock_token_' + Math.random().toString(36).slice(2);
console.log('[App] 异步请求完成,Token:', mockToken);
this.globalData.token = mockToken;
this.isFetchingToken = false;
resolve(mockToken);
}, 2000);
});
},
});
页面在 onShow 生命周期里检查全局数据。如果没有,就显示 loading,并调用 App 的方法去"重新获取"。
// pages/index.ts
Page({
data: {
token: '' as string | null,
loading: false,
pageState: '',
},
onLoad() {
console.log('[Page] onLoad');
},
onShow() {
console.log('[Page] onShow - 检查全局数据');
this.checkAndLoadData();
},
/**
* 核心逻辑:检查全局数据,未就绪则重新获取
*/
async checkAndLoadData() {
const app = getApp();
// 1. 检查全局数据是否就绪
if (app.globalData.token) {
console.log('[Page] Token 已存在,直接渲染页面');
this.setData({
token: app.globalData.token,
loading: false,
pageState: '数据已通过全局获取,页面渲染完成',
});
return;
}
// 2. 未就绪:显示 loading
console.log('[Page] Token 未就绪,显示 Loading 并重新获取');
this.setData({
loading: true,
pageState: '正在获取 Token,请稍候...',
});
// 3. 重新获取(调用 App 提供的公共方法)
try {
const token = await app.fetchToken();
this.setData({
token: token,
loading: false,
pageState: 'Token 获取成功,页面渲染完成',
});
// 这里可以继续调用依赖 token 的其他接口
// this.fetchUserProfile(token);
} catch (error) {
this.setData({
loading: false,
pageState: '获取失败,请重试',
});
}
},
// 用户手动重试按钮(可选)
onRetry() {
this.checkAndLoadData();
},
});
总结
本文围绕微信小程序的启动顺序与 App.onLaunch 的异步坑展开,核心要点如下:
-
启动顺序 :冷启动会完整执行
App.onLaunch → App.onShow → Page.onLoad → Page.onShow → Page.onReady;热启动(后台切回前台)仅触发App.onShow与Page.onShow,不会重新执行onLaunch与onLoad。 -
异步坑的本质 :
onLaunch中的异步请求(如获取 token/openId)不会阻塞后续生命周期,页面onLoad往往先于异步完成执行,导致直接读取全局数据时取不到值。 -
三种解决方案对比:
- Promise 方案 :在 App 中创建
loginReadyPromise,页面onLoad中await它,代码直观、适合单页面等待; - 回调/事件总线方案:App 维护回调队列,异步完成后统一通知所有订阅页面,适合多页面同时等待同一份数据;
- 页面内判断方案 :页面
onShow时检查全局数据,未就绪则显示 loading 并重新获取,实现简单、容错性好,但每个页面都要写一遍判断逻辑。
- Promise 方案 :在 App 中创建
-
选型建议:单页面依赖登录态优先用 Promise 方案;多页面共享同一份异步数据(如登录态、地理位置)推荐回调/事件总线;对实时性要求不高、希望页面自洽的场景可用页面内判断。
理解启动顺序与异步时序,是写出稳定、不踩坑的小程序代码的前提。希望本文的时序图与代码示例能帮你理清思路,在实际项目中少走弯路。