ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

小程序返回页不刷新?用onShow生命周期实现列表数据自动更新

小程序返回页不刷新?用onShow生命周期实现列表数据自动更新 最近接手一个小程序项目用户反馈最多的一个 bug 是“我在详情页改了资料返回列表页列表还是旧数据必须杀掉小程序重新进才正常。”这种问题在原生小程序、uni-app、Taro 项目里都很常见根子在于大多数开发者习惯把数据请求写在onLoad里而页面返回时根本不会重新触发onLoad。今天就把我用onShow处理这类“返回页刷新”的完整思路、代码和踩坑记录分享出来希望能帮你少走弯路。这篇文章适合正在开发小程序、uni-app 的开发者也适合被列表脏数据折磨的入门者和想优化页面数据同步的同学。既讲清楚生命周期原理也会给出可直接复制修改的完整代码最后还会聊聊我在实际项目里踩过的几个坑。1. 为什么返回按钮会让数据“失效”先看清楚页面栈与生命周期的执行顺序很多同学写小程序从第一天开始接触的就是onLoad里发请求、渲染页面于是形成惯性页面数据就是应该在onLoad里拿。但小程序页面的生命周期并没有那么简单它不是每次进入页面都重新走一遍onLoad。1.1 页面栈机制返回时页面并不会重新加载小程序是一个多页面的框架但它的页面管理方式不是浏览器那种“每次新建一个网页”而是维护了一个页面栈。打开小程序第一个页面栈底是A。从A跳到B栈变成A - B。从B跳到C栈变成A - B - C。当你点返回按钮C被销毁出栈栈变成A - B此时B页面重新显示出来。这里的重点来了B页面是重新显示而不是重新创建。B页面的onLoad只会在第一次创建时执行一次之后无论你从C返回多少次onLoad都不会再执行。所以如果你把数据请求写在onLoad里返回时自然拿不到最新的数据。常见误区是有人会用onShow重新请求但又担心每次都请求会不会太浪费于是把请求还留在onLoad只在onShow里加一个“刷新按钮”提示用户手动刷新。这样体验也很割裂。1.2onLoad、onShow、onReady的执行时机一图看懂我用一个表格来总结常见的几个页面生命周期触发时机生命周期触发时机触发次数适合做什么onLoad页面创建时整个页面生命周期只触发一次读取参数、初始化变量、创建一些不需要重复的对象onShow页面每次显示时每次进入、返回、切到前台时都可能触发数据刷新、统计曝光、检查登录态onReady页面首次渲染完成只触发一次初始化一些依赖渲染层完成的组件或 canvasonHide页面隐藏时每次隐藏都触发保存草稿、清理定时器onUnload页面销毁时只触发一次释放资源、清理事件监听从这张表可以看出要实现“返回时自动刷新”onShow是最合适的着落点。它的触发频次更高覆盖面也更广不仅能覆盖“从C返回B”的场景还能覆盖“小程序切后台再回来”的场景。1.3 冷启动、热启动一次容易忽略的细节还有一个容易踩的坑是编译模式和热启动。在开发者工具里你点击“编译”相当于冷启动会重新创建页面栈onLoad会执行一次onShow自然也会执行。但用户在手机上使用小程序时更多是热启动——小程序没被完全销毁只是切到了后台再从最近任务里点回来。这时候数组会怎样如果你只依赖onLoad那用户切回来之后看到的还是旧数据而onShow会照常触发这正好是自动刷新的关键。2. onShow 的核心地位一个生命周期函数解决一半同步问题知道了onShow的触发时机接下来的问题是怎么用好它才能既保证数据新鲜又不至于请求过多、页面闪烁。2.1 传统做法为什么不优雅事件通知、回调函数、全局变量在值之前我看到很多同事为了解决列表刷新走了不少弯路在B页面定义一个onPullDownRefresh提示用户手动下拉刷新但用户没有这个习惯。从C页面修改数据后通过全局globalData修改标志然后调用wx.navigateBack在B页面的onShow里读取标志再决定是否刷新。但标志管理容易混乱容易漏清理。使用EventChannel在返回前通过this.getOpenerEventChannel().emit(dataChanged)发起通知B页面需要提前监听。这套逻辑没有问题但需要写不少配套代码并且页面多了之后维护成本不低。这些做法的核心问题都在于把“刷新”当成一个需要手动通知的一次性动作。而onShow的思路完全不同它把“页面每次显示的时机”当作天然的刷新信号不需要依赖上游页面主动配合更通用也不容易遗漏。2.2 “单数据源”思想把数据获取抽成一个方法而不是依赖某个生命周期我在项目中逐渐养成了一个习惯页面中凡是可重复执行的获取数据逻辑不直接放在onLoad/onShow里而是抽成一个独立的fetchData()方法。Page({ data: { list: [], loading: false, }, onLoad(options) { // 首次读取参数 this.pageId options.id; // 先集中一次请求 this.fetchData(); }, onShow() { // 返回页面时需要重新请求 this.fetchData(); }, fetchData() { if (this.loading) return; this.setData({ loading: true }); requestList({ id: this.pageId }) .then((res) { this.setData({ list: res.data }); }) .finally(() { this.setData({ loading: false }); }); }, });这段代码的逻辑是首次进入页面时onLoad和onShow都会触发一次fetchData会执行两次吗确实会所以需要做一次保护。上面的代码用this.loading做了一个简单的防重入。不过它只能防止同时并发连续两次进入时loading可能已经变回false所以仍需更细致的判断。更精细的方案是只依赖onShow来处理所有显示场景onLoad里只处理参数初始化不重复调用数据请求。Page({ onLoad(options) { this.pageId options.id; this._hasLoaded false; }, onShow() { if (this._hasLoaded !this.shouldRefreshOnShow()) { return; } this.fetchData(); this._hasLoaded true; }, });这样首次进入和后续返回会走不同的分支下面一章详细讲这个逻辑。2.3 为什么这种模式在长列表、详情页场景里特别顺手对于列表页 详情页这类场景数据变化往往发生在第二级页面。例如订单列表点击去支付充值成功返回再点另一个订单查看列表的最新状态需要刷新。onShow模式天然适配这种“每次都有可能有新变化”的场景不用关心用户到底在哪里改了数据只要页面一重新出现就拉一次最新数据保持状态一致性。3. 一步步实现从最简代码到完整防误触版本下面我会从最简单的写法开始再逐步加上各种边界保护最终给出一份可直接用到业务里的完整代码。3.1 最简实现在 onShow 里直接调接口先说最直白的版本适合只有一两个接口的小页面Page({ data: { list: [], isLoading: false, }, onShow() { this.isLoading true; wx.showLoading({ title: 加载中 }); fetch(/api/getList) .then((res) { this.setData({ list: res.data }); }) .finally(() { this.isLoading false; wx.hideLoading(); }); } })优点代码简单理解成本低。缺点每次onShow都会无差别请求即使用户只是从详情页返回啥也没改也会白白消耗流量和渲染资源。所以我一般不建议项目里直接使用这种裸版本但小 demo 或者内部工具类小程序可以毕竟省事儿。3.2 增加“是否需要刷新”的判断控制请求频次大部分业务场景是用户从B进入C只有C里发生了数据变更返回B时才有必要刷新。如果只是误点击进入或者返回后数据没变刷新就是多余动作。我的做法是给页面增加一个“脏标记”由来源页面决定是否触发刷新Page({ data: { list: [], }, onLoad(options) { this.pageId options.id; this._needRefresh true; // 首次进入需要加载 }, onShow() { if (!this._needRefresh) return; this.fetchData(); this._needRefresh false; }, onSomeBusinessAction() { // 某些页面内操作可能产生数据变化手动置位 this._needRefresh true; }, fetchData() { // 异步获取列表数据 wx.request({ url: https://api.example.com/list, data: { pageId: this.pageId }, success: (res) { this.setData({ list: res.data.list }); }, }); }, });但是这种“脏标记”方式同样需要上游配合因为我们不知道用户什么时候返回。更常见的做法是结合globalData或者事件总线来判断。比如详情页修改数据后设置一个globalData.needRefreshList true列表页onShow时读取这个标记const app getApp(); Page({ onShow() { if (app.globalData.needRefreshList) { this.fetchData(); app.globalData.needRefreshList false; } }, });缺点是这个needRefreshList是个全局变量页面多了容易互相污染。所以我更推荐下面这种更稳的思路。3.3 完整可用版本带 loading、防重入、防首次重复请求下面是我目前比较常用的姿势使用onShow统一处理首次进入时onShow必然触发所以不需要在onLoad里请求数据onLoad只负责参数解析和状态初始化。配合_loading防止重复请求配合_loaded记录是否已完成首次加载。Page({ data: { list: [], loading: false, page: 1, hasMore: true, }, onLoad(options) { this._pageId options.id; this._loaded false; // 是否完成首次加载 this._loading false; // 是否正在请求中 this.setData({ page: 1, hasMore: true, list: [] }); }, onShow() { // 首次进入直接加载 if (!this._loaded) { this._loaded true; this.fetchList(true); return; } // 非首次进入视业务决定是否直接加载 // 可以加一些判断也可以无脑刷 this.fetchList(true); }, async fetchList(reset false) { if (this._loading) return; this._loading true; const page reset ? 1 : this.data.page; if (reset) { this.setData({ loading: true }); } try { const res await requestList({ id: this._pageId, page, pageSize: 10, }); this.setData({ list: reset ? res.list : this.data.list.concat(res.list), page: page 1, hasMore: res.hasMore, }); } finally { this._loading false; this.setData({ loading: false }); } }, onReachBottom() { // 触底加载下一页 if (this.data.hasMore !this._loading) { this.fetchList(false); } }, });这份代码的优点在于首次进入和返回刷新走同一套逻辑避免双份代码。用this._loading防止快速返回、下拉刷新、触底加载三个动作同时触发多个请求。reset参数决定是覆盖列表还是追加列表方便触底分页。3.4 附赠一个可复用的“onShow 自动刷新”工具函数如果项目里很多列表页都需要这个逻辑我不建议每个页面都复制粘贴。推荐抽一个简单的 mixin / composable。原生小程序里可以用behavior// behavior/auto-refresh.js module.exports Behavior({ data: { autoRefresh: true, }, lifetimes: { attached() { this._needAutoRefresh true; }, }, methods: { triggerRefresh() { if (typeof this.fetchData ! function) return; this._needAutoRefresh true; if (typeof this.onShow function) { this.onShow(); } else { this.fetchData(); } }, }, pageLifetimes: { show() { if (this._needAutoRefresh this.data.autoRefresh) { this._needAutoRefresh false; this.fetchData this.fetchData(); } }, }, });这个 behavior 只是思路实际使用中你还需要处理首次加载、防重入等问题。但核心思路是show生命周期统一拦截业务方只需要维护fetchData方法。4. 细节决定体验那些容易翻车的边界情况onShow自动刷新看似简单但在真实项目中会遇到各种“衍生 bug”。我挑几个高频的坑仔细聊聊。4.1 首次进入也走 onShow如何避免重复请求这是最常见的困惑。正如前面的代码所示如果你在onLoad里也发了一次请求在onShow里又发一次页面刚打开会请求两次。解决方案就是不要把onLoad当请求入口而是把onLoad当作“只初始化参数和状态”真正的数据获取统一交给onShow。首次进入时_loaded是falseonShow里fetchData之后置为true后续返回时就不会再走首次逻辑。这样代码里只有一条数据流调试起来更清晰。4.2 onShow 也会因为小程序切后台触发如何区分用户来源小程序从后台切回前台时onShow同样会触发。比如用户正在列表页突然一个新闻 App 弹窗呼走过了两分钟切回来页面会莫名刷新一次如果恰好处于某个滚动位置刷新后滚动位置很可能会丢失体验大打折扣。如果业务上希望区分这种场景可以在onHide里记录一个时间戳在onShow里判断前后间隔Page({ onHide() { this._hideTime Date.now(); }, onShow() { const now Date.now(); if (this._hideTime now - this._hideTime 3000) { // 短时间内从子页面返回忽略 return; } this.fetchData(); }, });但这个方法并不完美比如用户真的在 3 秒内从详情页返回也可能会被忽略。更合理的方式是结合页面栈状态判断。不过大多数项目里简单的“仅保留一次比较”已经够用。4.3 修改了详情页的数据返回时列表如何知道要刷新前面提过globalData和“脏标记”但更健壮的方式是使用事件总线。原生小程序没有内置事件总线但可以利用wx.event或自己封装一个简单的发布订阅// utils/bus.js const listeners {}; function on(eventName, callback) { (listeners[eventName] listeners[eventName] || []).push(callback); } function off(eventName, callback) { if (!listeners[eventName]) return; listeners[eventName] listeners[eventName].filter((cb) cb ! callback); } function emit(eventName, payload) { (listeners[eventName] || []).forEach((cb) cb(payload)); } module.exports { on, off, emit };详情页修改数据成功后const bus require(../../utils/bus); // 修改成功后 bus.emit(listChanged, { id: this.data.detailId });列表页在onShow里订阅并刷新const bus require(../../utils/bus); Page({ onLoad() { this._handleChanged () { this.fetchData(); }; bus.on(listChanged, this._handleChanged); }, onShow() { // 不一定每次显示都请求但需要注册监听 }, onUnload() { bus.off(listChanged, this._handleChanged); }, fetchData() { // 实际请求 }, });这套模式比globalData表现力更强而且不会出现多个页面互相污染。4.4 图片闪动、列表滚动位置丢失刷新时的渲染优化全量setData替换列表会导致页面重新渲染用户正在阅读的位置可能被顶掉尤其是一些信息流场景。处理思路有三种如果数据量不大可以对比新旧数据只setData发生变化的部分但成本较高。页面进入时先记录滚动位置刷新完成后用wx.pageScrollTo滚回原位置。增加一个过渡态比如在列表顶部显示“数据已更新”的小条给用户视觉反馈而不是让列表突然跳一下。这三种方式可以结合使用核心目标是让用户觉得“数据自动更新了但体验依然平滑”。5. 工程化扩展在 uni-app、Vue3 组合式函数中复用这套逻辑现在很多项目不是用原生小程序而是用 uni-app 或 Taro。这里的写法略有区别但核心思路完全一致。5.1 uni-app 中的 onShow 页面生命周期uni-app 基于 Vue 2/Vue 3页面生命周期里有onShow但在 Vue 3 组合式写法里需要从dcloudio/uni-app里引入onShowscript setup import { ref } from vue; import { onShow } from dcloudio/uni-app; const list ref([]); const loading ref(false); let loaded false; onShow(() { if (!loaded) { loaded true; fetchList(); } else { fetchList(); } }); async function fetchList() { loading.value true; try { const res await uni.request({ url: https://api.example.com/list, }); list.value res.data.list; } finally { loading.value false; } } /script注意在 Vue 3 的script setup中使用onShow时他其实是在当前页面实例上注册的同一个页面里可以有多个onShow钩子它们会依次执行。这给了我们一个好处可以把数据刷新逻辑拆分到多个 composable 中互不干扰。5.2 把自动刷新逻辑抽成 composable在很多项目中列表页、详情页、个人中心页都会有类似的“显示时刷新”逻辑。我建议把它抽成一个组合式函数方便复用。// composables/useAutoRefresh.js import { onShow, onLoad } from dcloudio/uni-app; export function useAutoRefresh(fetcher, options {}) { const data ref(options.initialData || null); const loading ref(false); let loaded false; let shouldRefresh true; const fetchData async (...args) { if (loading.value) return; loading.value true; try { data.value await fetcher(...args); } finally { loading.value false; } }; onLoad(() { loaded false; shouldRefresh true; }); onShow(() { if (shouldRefresh || !loaded) { fetchData(); loaded true; shouldRefresh false; } }); const triggerRefresh () { shouldRefresh true; fetchData(); }; return { data, loading, fetchData, triggerRefresh, }; }使用方式很简洁script setup const { data: list, loading, triggerRefresh } useAutoRefresh(() { return requestList({ page: 1, pageSize: 10 }); }); /script抽出来以后页面代码变得很干净需求变了也只需要改一处。5.3 与下拉刷新、定时刷新的组合onShow自动刷新适合“进入页面时同步最新状态”但用户停留在一个页面很长时间也可能需要周期或手动刷新。常见组合方式onShow触发一次即时刷新。onPullDownRefresh触发下拉刷新。如果页面有一套监控大屏之类的应用可以用setInterval配合onHide清理定时器。需要注意的是要在onHide或onUnload中清理定时器避免页面在后台时频繁发请求。Page({ onShow() { this.fetchData(); this.startPolling(); }, onHide() { this.stopPolling(); }, onUnload() { this.stopPolling(); }, startPolling() { this.stopPolling(); this._timer setInterval(() { this.fetchData(); }, 30000); }, stopPolling() { if (this._timer) { clearInterval(this._timer); this._timer null; } }, });这里有个细节setInterval的间隔不要太短否则列表数据量较大时滚动和交互都会卡顿。我自己一般给 30 秒以上。6. 经验小结这套方案的取舍与适用边界最后写几句我个人实际用过之后的感受。onShow自动刷新并不是银弹它适合数据来源相对单一且实时性要求较高的列表页、详情页、个人中心页。如果页面数据非常庞大或来源需要复杂计算那每次显示都刷新可能会带来性能压力此时可以配合事件总线或者“脏标记 时间戳”做进一步过滤。在我维护的几个小程序里把列表刷新逻辑统一切到onShow之后最大的变化是排班、订单状态这类场景几乎不会再出现“数据过期”的客诉。以前靠用户下拉刷新现在只要返回列表页数据就自动是最新的。这种体验虽然不张扬但用户能明显感觉到“这个程序很懂我”。一个小技巧是在开发者工具里可以开启onShow的日志监听看看页面跳转时到底触发了多少次。调试onShow被频繁触发的问题时这个日志是排第一的帮手。希望这套方案能帮你在下一个项目里少掉几根头发。如果你也遇到过返回列表不刷新的问题不妨先试试把请求放到onShow不复杂但真的很管用。
返回列表