基本介绍
onShow、onLoad与onReady都是小程序页面生命周期函数。
onLoad 在页面加载时调用,仅一次;
onShow页面显示/切入前台时触发,两个生命周期非阻塞式调用。
onReady 是页面初始化数据已经完成后调用的,并不意味着onLoad和onShow执行完毕。
调用顺序是onLoad > onShow > onReady
根据对应的执行机制,我们预期有三种执行的逻辑
A. 页面每次出现都会执行
- 从其他页面返回
- 手机锁屏唤醒,重新看到小程序页面
- 把当前小程序页面重写切换到前台(多任务)
B. 页面加载后只需执行一次(页面第一次载入)
C. 只在页面非第一次执行时才执行(A情况的子集,页面非第一次展示时)
需求与问题
逻辑1:
因为onLoad和onShow是非阻塞执行的,当我们有一个这样的需求:页面载入执行A方法,页面展示执行B、C、D方法时,A需要在BCD之前执行,此时把A放在onLoad中,BCD放在onShow中就无法实现需求
逻辑2:
还有一种需求是:页面第一次执行A,非第一次执行R-A,这里onLoad和onShow并没有非第一次的逻辑,需要手动判断。
一种实践方法
下面是纯粹使用onShow代替onLoad,完成所有逻辑的示例,保证了业务逻辑的执行顺序可控。
options获取使用其他方式代替。
为了保持onShow中逻辑的清晰性,尽量使用EventChannel去替代原本onShow+globalData的逻辑。
data:{
first: true
},
async onShow(){
//代替onLoad中的options的获取
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const options = currentPage.options;
this.funD() // C2 页面每次都调用的逻辑
if(this.data.first){
this.data.first = false;
await this.funA(); //A 仅在页面初次调用的逻辑(按需是否阻塞调用)
}else{
await this.funB(); //B 仅在页面非初次时调用的逻辑
}
await this.funC(); //C1 页面每次都调用的逻辑
}
另外一种使用实践
data:{
first: true
}
onShow(){
this.funD() //页面每次都调用的逻辑(仅非阻塞)
if(!this.data.first){
this.funC() //仅在页面非初次时调用的逻辑
}
await this.funE() //页面每次都调用的逻辑(可阻塞,可非阻塞)
},
onLoad(){
//仅在页面初次调用的逻辑
this.funA();
await this.funB();
}
onReady(){
this.data.first = false;
}
如有错误,恳请指出。
实践1中,设置 first 后执行 funA, 如果失败了怎么办?要不要再次执行?
实践2中,onReady 和函数执行没有确定的时间关系,此处设置 first 作用不明确
第一个问题:实际await都应该用try...catch包裹的,即使失败也不会影响正常流程;
第二个问题onReady确实和函数执行没有时间关系,但这里总体是onShow是在onReady之前执行的,而对于if(!this.data.first)是在同步代码中的,所以是可以保证这个判断总在onReady之前执行的。
第二个问题,要求funD 必须为同步函数,这就是定编码规则了,必须人工核查,这个稍微有点费劲
第二个问题:要求funD为同步函数,确实不是强制规则,这里的原则是约定大于配置,也就是说让开发人员自己遵守即可。
data:{
first: "todo"
},
async onShow(){
this.funD() // C2 页面每次都调用的逻辑
if(this.data.first == "todo"){
this.data.first = "pending";
await this.funA()
.then(() => this.data.fisrt = "finished")
.catch(() => this.data.first = "todo"); //A 仅在页面初次调用的逻辑(按需是否阻塞调用)
} else if (this.data.first == "finished"){
await this.funB(); //B 仅在页面非初次时调用的逻辑
}
await this.funC(); //C1 页面每次都调用的逻辑
}
// util.js
const onceState = (page, f) => {
if page.data.state == "todo" {
page.data.state = "pending"
return f()
.then(()=> page.data.state = "finished")
.catch(() => page.data.state = "todo")
} else if (page.data.state == "pending") {
return Promise.reject("state is pending")
} else {
return Promise.resovle("already finished") })
}
}
const onlyStateReady = (page, f) => {
if page.data.state == "finished" {
return f()
}
return Promise.reject("state not ready")
}
// xxx_page.js
onShow() {
onceState(this, this.funA)
.then(() => onlyStateReady(this, this.funB))
.then(...)
}
文中逻辑和代码逻辑说得有点复杂,最开始的部分比较清晰,后面的代码实践处理 first 还是会存在隐患。
归总,问题是两个,一是时机,二是顺序。时机只能选择,顺序问题是唯一要解决的问题。
顺序问题可以通过 Promise 解决,比较轻松。
只执行一次的问题,可以通过状态检查,放在顺序中。
// onLoad -> A,B,D,Mark
// onShow -> A,D (防重)
//
// 下面示例中 A 函数为 user.OnReady
// 防重有两种方式,一是状态防重,设置和检测init,二是函数防重,在 user.OnReady 中内置,复用结果数据
// one_page.ts
data {
init: 0
},
onLoad() {
user.onReady()
.then(() => this.loadMemberSetting())
.then(() => this.loadMemberList())
.then(() => this.setData({ init: 1 }))
},
onShow() {
// 一种防止重复执行 onLoad 函数的方式
if(this.data.init == 1) {
// 一种复用结果的方式
user.onReady()
.then(() => this.loadMemberList())
}
},
loadMemberSetting() {
return user.get("/api/member/setting").then((res: any) => console.log(res))
},
loadMemberList() {
return user.get("/api/member/list").then((res: any) => console.log(res))
}
// user.ts
export const onReady = new Promise((resovle, reject) => {
const user = wx.getStorageSync("user") || {}
if(user.id) {
resovle(user)
} else {
// ... 登录并获得用户数据,如果失败...
}
})