onShow + onReady 同时拉数据,微信小程序页面首次加载 API 被调两次
给页面加"返回自动刷新"需求时,我习惯在 onShow 里调一次数据方法;而 onReady 里本来就有登录检查 + 初始化拉取。写完上线,打开 Network 面板一看——同一个接口连续打了两条请求,间隔不到 50ms。服务端日志也证实了:首次进页面,数据接口被调用了两次。
现象
- 打开 DevTools 的 Network 面板,进入该页面的瞬间,同一个 API 端点出现两条紧挨着的请求。
- 页面偶尔出现短暂闪烁:先渲染一次,再刷新一次,内容抖了一下。
- 离开页面再返回时,接口只打一次(行为正常)。
规律很明显:只在首次进入时重复,返回时不重复。
根因
微信小程序页面的生命周期顺序是:
首次进入:onLoad → onShow → onReady
tab 切换:onShow(仅此一个)
navigateBack:onShow(仅此一个)
wx.reLaunch:onLoad → onShow → onReady(完整循环)
关键反直觉点:onShow 比 onReady 先触发。
很多人(包括我)直觉上以为 onReady 是"渲染完成"、onShow 是"每次显示",所以在 onReady 做初始化、onShow 做刷新,看起来天经地义——实际上首次进入时两个钩子都会触发,而且 onShow 在前。
首次进入页面时,onLoad → onShow → onReady 三个钩子全部触发。如果 onShow 和 onReady 都调用了同一个数据方法,就会产生两次请求。这个问题在"返回刷新"需求加进来之前不会暴露,所以往往是后期追加功能时才踩到。
正解
用一个实例标志位 _hasLoaded 区分「首次加载」和「后续 onShow」:
Page({
onLoad(options) {
// 解析参数,设置初始状态
},
onReady() {
const that = this
// 首次加载:做登录检查、权限弹窗等初始化,然后拉数据
app.checkLogin(function () {
that.loadData()
})
},
onShow() {
// 只在「返回」时刷新,首次进入由 onReady 负责
if (this._hasLoaded) {
this.loadData()
}
},
loadData() {
const that = this
api.getData(params).then(function (res) {
that._hasLoaded = true // 成功后才打标,失败时 onShow 仍会重试
that.setData({ /* ... */ })
})
},
})三个关键决策:
_hasLoaded在异步成功回调里赋值,而不是在onReady入口。这样如果首次加载失败(网络错误、登录跳转),用户返回后onShow还会再试一次,不会静默跳过。onReady负责首次加载,onShow负责返回刷新——各司其职,不重叠。- 标志位挂在实例上(
this._hasLoaded),不放data里,避免触发不必要的setData渲染。
onReady 无条件拉数据(首次),onShow 检查 _hasLoaded 后才拉(返回刷新)。标志位在异步成功回调里设置,保证失败可重试。这个模式适用于原生 Page、Taro、uni-app,以及 Omix create.Page(store, {...}) 封装——底层都是标准 Page 构造器。
附:入场动画闪烁的同类坑
如果页面有 CSS 入场动画(opacity 0→1、translateY)通过 pageReady 标志位驱动,onShow 每次把 pageReady 重置为 false 再异步设回 true,用户返回时所有动画元素会短暂消失再淡入——视觉上像白屏了一下。
解法同理,加一个 _hasShown 标志:
onShow() {
const resetData = { /* 其他需要重置的字段 */ }
// 只在首次进入时重置入场动画
if (!this._hasShown) {
resetData.pageReady = false
this._hasShown = true
}
this.setData(resetData)
// 加载数据后再把 pageReady 设回 true
}_hasLoaded 控制「要不要请求数据」,_hasShown 控制「要不要重置动画」,两个问题独立,可能同时需要。
一句话外卖
微信小程序生命周期顺序是
onLoad → onShow → onReady,onShow先于onReady执行;「返回刷新」逻辑加进onShow之前,先用_hasLoaded标志位把首次和后续区分开。