ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

小程序列表页返回刷新:彻底搞懂onShow生命周期与页面栈机制

小程序列表页返回刷新:彻底搞懂onShow生命周期与页面栈机制 做小程序开发这几年“列表页返回不刷新”这个问题我见过不下十种错误写法。有人在onLoad里硬刚发现返回时纹丝不动有人把请求塞进onPullDownRefresh然后设置成自动下拉体验别扭还有人走极端每次onShow里无脑全量刷新结果页面闪个不停。实际上微信小程序官方早就在生命周期里留好了口子onShow这个生命周期函数就是为“页面每次亮出来”而生的配合返回按钮的页面栈行为可以实现非常优雅的数据自动刷新。这篇文章想跟你把onShow刷新这件事聊透。不只会给你能直接抄的完整代码还会讲清楚背后的生命周期规律、三种从简到繁的实现方式、进阶的全局刷新机制以及我在真实项目里踩过的一堆坑。无论你是刚接触小程序的新手还是已经写了两三年的老手只要遇到“从详情页返回列表页要刷新”“编辑页保存后回到首页要同步”这类需求这篇都能给你一套靠谱的方案。1. 为什么返回刷新总离不开 onShow先搞清楚页面生命周期的触发规律1.1 onLoad 和 onShow 的区别一个只走一次一个每次现身都走很多新人刚开始都搞混这两个生命周期函数。onLoad是页面第一次加载时触发的整个页面实例的生命周期里只执行一次。这很重要——当你在页面AnavigateTo跳转到页面B再通过返回按钮回到A时A 其实早就加载过了它一直静静躺在页面栈里根本不会重新触发onLoad。这就是为什么“在 onLoad 里请求数据”对返回场景完全无效。onShow则完全不同。只要页面从“隐藏状态”变成“显示状态”它就会触发。什么叫隐藏转显示最常见的就是从 B 页面返回 A 页面A从栈底重新露出来还有从后台切回小程序、从 A 跳转小程序内置的授权弹窗后弹窗关闭这些场景都会触发onShow。可以说onShow才是真正对应“用户看到了这个页面”的时机所以“返回时让页面数据重新拉取”从生命周期层面讲就应该放在onShow里。我的建议很简单首次加载的数据请求你愿意放onLoad就放onLoad但如果这个页面需要被别的页面返回并刷新那就干脆在onShow里统一管理请求。别两处一起写写了一起触发新手最容易在这里翻车。1.2 返回按钮“隐藏玩法”的本质页面栈的行为规则要说清楚为什么onShow能接住返回按钮的刷新需求就得理解小程序的页面栈机制。每次navigateTo相当于把一个新页面压入栈顶返回值按钮是navigateBack把当前页面弹出栈。关键点在于当栈顶的 B 被弹出时A 并没有被销毁而是重新成为栈顶页面。此时微信会先触发 A 的onShow然后页面渲染。所以从时序上看返回按钮这个动作天然就是“上一页 onShow”的触发源。说它是“隐藏玩法”其实一点不隐藏只是时间长了大家自然而然总结出来的规律。你要做的就是在onShow里接住这个时机去检查数据是否需要更新然后决定要不要重新请求接口。这里有个常见的认知误区以为navigateBack会刷新上一页。其实navigateBack本身不会传任何数据给上一页上一页的data保持原样。如果你不做任何处理页面上显示的就永远是旧数据。这也是为什么很多后台管理系统里“保存之后列表没更新”的问题本质上就是没有正确处理这个返回时机的刷新。1.3 别急着写代码先记住 onShow 的三个副作用在你满怀热情往onShow里塞请求之前先把这三件事记在心里后面能帮你少掉很多头发。第一onShow不只返回时触发。用户把小程序切到后台再切回来也会触发当前页面onShow。这意味着如果你的刷新逻辑特别重用户只是去回了条微信消息回来就要等半天加载——这种体验非常糟糕。所以后面我会讲带条件的刷新而不是无脑刷新。第二onShow里写wx.showLoading要谨慎。因为onShow触发的频率比onLoad高得多每次跳转回来都会转圈。大多数场景下静默刷新就够了数据到了直接更新视图用户感知不到体验才自然。第三onShow里 setData 也不能肆无忌惮。如果每次回来都把一个两三千条的大数组整个 setData 一遍性能会有明显损耗特别是在低端安卓机上页面会有肉眼可见的卡顿。正确姿势是只更新真正变化的部分或者用局部路径更新。当然这些副作用不是说不能碰onShow而是要带着设计思维去用它。接下来就看看具体怎么写代码。2. 从简到繁的代码方案onShow 刷新数据的三套写法2.1 无脑刷新版适合列表页先给最简单的版本。如果你对性能没有极致要求页面数据量不大也懒得设计复杂的刷新条件可以直接在onShow里重新请求数据。这种写法最适合“列表页 → 详情页 → 返回列表页”的结构化场景。// pages/list/list.js Page({ data: { list: [], loading: false, }, onLoad() { // 首次进入交给 onShow 统一处理onLoad 不用重复请求 }, onShow() { this.fetchList(); }, fetchList() { this.setData({ loading: true }); wx.request({ url: https://api.example.com/list, method: GET, success: (res) { if (res.statusCode 200) { this.setData({ list: res.data.list }); } }, complete: () { this.setData({ loading: false }); }, }); }, });这个版本看起来很简单但有一个隐藏问题如果用户在详情页或编辑页进行了操作返回列表页列表会刷新但用户什么都没做只是进去看了一眼就退出来列表也照样重新请求了一遍。对于轻量接口这无所谓但如果列表接口本身比较重或者有大量图片资源需要重新渲染这种无脑刷新会让用户觉得“每次回来都在加载好烦”。如果你能接受这个小缺点那么这个版本可以直接用。毕竟代码越简单出 bug 的概率越低。而且用户习惯之后也不会特别在意那几百毫秒的加载。2.2 条件刷新版用全局标记控制是否刷新聪明的做法是让“上一页”告诉“当前页”——我这次返回到底需不需要刷新。常用的载体是app.globalData或者wx.setStorageSync。这里我用一个全局标记位needRefresh来演示。具体逻辑在编辑页或详情页执行了修改操作比如保存、删除、收藏之后把全局标记设成true等返回列表页时列表页发现标记是true就会执行刷新然后把标记重新置为false避免下次返回重复刷新。// app.js App({ globalData: { needRefresh: false, }, });// pages/edit/edit.js Page({ onSave() { // 模拟保存操作 wx.request({ url: https://api.example.com/save, method: POST, success: () { // 关键保存成功后打一个标记 getApp().globalData.needRefresh true; wx.navigateBack(); }, }); }, });// pages/list/list.js Page({ data: { list: [], }, onShow() { const app getApp(); if (app.globalData.needRefresh) { app.globalData.needRefresh false; this.fetchList(); } }, fetchList() { // 请求接口并更新 list }, });这种写法的好处是精准——只有确实发生数据变更的返回才会触发刷新用户只是浏览返回不会打扰他。缺点是全局标记位需要你维护好而且如果你的页面很多全局变量会变得很乱。所以我一般建议用事件通道的方式把刷新指令更精准地传给目标页面这点后面第四节会详细讲。2.3 带交互提示的刷新避免页面“假死”错觉有时候接口响应比较慢如果走静默刷新页面一直显示旧数据用户摸不清到底有没有在更新就会下意识再点几下。这时候就需要一个轻量级的交互反馈。我个人比较推荐的方式是用wx.showNavigationBarLoading在导航栏显示一个小圆圈加载状态而不是弹一个全屏的showLoading。因为showLoading是模态的会阻断用户操作在返回流程里特别容易造成“卡死”的错觉。// pages/list/list.js Page({ data: { list: [], }, onShow() { this.refreshWithTip(); }, refreshWithTip() { wx.showNavigationBarLoading(); this.fetchList({ onComplete: () { wx.hideNavigationBarLoading(); }, }); }, fetchList({ onComplete }) { wx.request({ url: https://api.example.com/list, success: (res) { this.setData({ list: res.data.list }); wx.showToast({ title: 数据已更新, icon: success, duration: 800, }); }, complete: onComplete, }); }, });这里有个小技巧complete回调里用匿名函数包一层onComplete可以确保请求无论成功还是失败导航栏的 loading 都能被正常关掉。另外 toast 提示我只建议在“确实发生了数据变更”的刷新里用如果只是普通浏览返回也弹 toast用户会觉得很聒噪。3. 进阶玩法把“数据自动刷新”做成一套可复用机制3.1 eventChannel 方案返回时主动通知上一页页面间通信的方法很多但如果你用的微信基础库版本在 2.7.3 及以上强烈建议试试eventChannel。它的思路是navigateTo的时候从页面A传到页面B一个events对象B 页面可以通过getOpenerEventChannel().emit()主动调用 A 页面预先定义的回调。这样“刷新指令”就成了页面间的显式事件比全局变量清晰得多。来看一个具体例子。列表页 A 打开详情页 BB 里做了收藏操作B 返回 A 前主动通知 A 刷新列表。// pages/list/list.js Page({ data: { list: [], }, onShow() { // 这里不做无条件刷新等事件通知 }, openDetail() { wx.navigateTo({ url: /pages/detail/detail?id123, events: { refreshList: () { this.fetchList(); }, }, }); }, fetchList() { // 请求接口并更新 list }, });// pages/detail/detail.js Page({ onCollect() { // 执行收藏逻辑 const eventChannel this.getOpenerEventChannel(); if (eventChannel) { eventChannel.emit(refreshList); } // 然后返回 wx.navigateBack(); }, });这套写法的好处非常明显刷新指令直达目标不需要维护任何全局状态而且事件的创建和消费在代码里一目了然。需要注意的是eventChannel只能用在navigateTo打开的新页面redirectTo、navigateBack都拿不到。如果业务里“详情页修改完必须刷新列表”是强需求我个人会优先用这个方案。它比全局标记位优雅也比storage记录更不容易出错——不用关心什么时候清理过期标记。3.2 用 Behavior 抽离 onShow 刷新逻辑项目大了之后你会发现“列表页返回刷新”这个逻辑会在很多页面里重复出现。与其每个页面复制粘贴一段onShow判断逻辑不如把所有公共部分抽到一个Behavior里。微信小程序原生支持Behavior它有点像混入mixin可以把生命周期、方法、数据合并到页面里。下面这段代码实现了一个“带刷新开关”的 Behavior。页面在 data 里声明一个refresherEnabled开关Behavior 会在onShow时检查开关然后调用页面自己实现的fetchData方法。这样每个列表页只需要关心自己的fetchData怎么写不用再写那些重复的onShow判断。// behaviors/refresh.js module.exports Behavior({ data: { refresherEnabled: true, }, onShow() { if (this.data.refresherEnabled) { this.fetchData this.fetchData(); } }, });// pages/list/list.js const refreshBehavior require(../../behaviors/refresh); Page({ behaviors: [refreshBehavior], data: { list: [], }, fetchData() { wx.request({ url: https://api.example.com/list, success: (res) { this.setData({ list: res.data.list }); }, }); }, });我看到有很多人用组件方式来做类似的事但遇到“页面级生命周期”这种场景Behavior 才是原生且轻量的解法。组件里写onShow只能在options: { virtualHost: true }之类的特殊条件下才能感知页面生命周期限制比较多直接上Behavior不会有这种问题。3.3 自定义导航栏场景手动返回按钮怎么处理现在不少项目用了自定义导航栏在app.json里设置navigationStyle: custom来实现沉浸式效果或个性化标题。这时候页面左上角就没有系统返回按钮了你需要自己在代码里写一个返回图标绑定navigateBack事件。很多人会问这种手动返回和onShow刷新会不会有冲突答案是不会。因为navigateBack只是触发返回动作返回后上一页照样会走onShow你的刷新逻辑依然生效。但这里有个容易埋雷的地方自定义导航栏会导致页面顶部高度不可预测如果处理不当容易误触返回按钮造成非预期的返回。我习惯在onLoad里读取系统信息算出状态栏高度和导航栏高度之后再动态设置返回按钮的定位。稳妥的做法是用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置让自定义返回按钮和胶囊垂直居中保持一致视觉上才协调。Page({ data: { navBarHeight: 44, statusBarHeight: 20, }, onLoad() { const systemInfo wx.getSystemInfoSync(); const menuRect wx.getMenuButtonBoundingClientRect(); this.setData({ statusBarHeight: systemInfo.statusBarHeight, navBarHeight: (menuRect.top - systemInfo.statusBarHeight) * 2 menuRect.height, }); }, goBack() { const pages getCurrentPages(); if (pages.length 1) { wx.navigateBack(); } else { wx.switchTab({ url: /pages/index/index, }); } }, });如果你用了自定义导航栏上面的goBack方法一定要写“无上一页可返”时的兜底逻辑。很多新手只写navigateBack结果从分享链接或者扫码进入时页面栈深度不够点了返回按钮没反应用户就会觉得 App 卡了。4. onShow 触发时机与性能优化细节决定体验4.1 初次进入、后台切回、弹窗切换都会触发 onShow先看看onShow到底哪些场景会触发。我把常见触发场景列在下面触发场景说明是否需要刷新页面首次加载显示与 onLoad 几乎同时触发需要从下级页面返回通过 navigateBack视情况而定从后台切回小程序用户切走微信再切回来视情况而定打开授权弹窗后关闭比如地理位置授权、手机号授权一般不需要打开客服会话后返回右上角菜单里的客服一般不需要从 tabBar 切换回本页不同 tab 页面之间切换视情况而定这张表的意思很明确不是所有onShow触发都需要重新拉接口。如果你把上面这些场景全部一视同仁地刷新不仅浪费流量还会让页面闪烁。我的做法是把“刷新条件”独立成一个方法比如shouldRefresh()返回布尔值来决定当前这次onShow是否为有效刷新时机。后续就算产品经理说“切后台回来也要刷新”你也只需要改这一个方法。4.2 setData 别整块赋值局部路径才是正道写onShow刷新的时候最常见的性能杀手就是this.setData({ list: newArray })这种整块赋值。如果newArray特别大会触发整个列表组件的重新渲染用户能明显感觉到页面跳动。微信小程序setData支持路径表达式你可以只更新某个字段。比如请求返回的新列表第一项发生了变化可以用下面的方式做精确更新setList (list) { if (!Array.isArray(list)) return; const patch {}; list.forEach((item, index) { patch[list[${index}]] item; }); patch[list.length] list.length; this.setData(patch); };这里我特意把list.length一起更新是因为小程序动态渲染数组时长度字段也参与状态维护。如果发现页面列表实际数量比数据少或者多多半是漏了这个字段。不过说实话为了代码可读性大部分场景我还是直接this.setData({ list })只有列表超过几百条、或者每条内容比较复杂比如包含图片、富文本的时候我才会用上面的路径更新方案。性能优化也要看投入产出比别为了节约一点渲染时间写出让人看不懂的代码。4.3 基础库与按需注入对 onShow 的影响微信小程序自定义组件和页面生命周期受基础库版本影响不大onShow从早期版本就一直存在。但有两个配置项会直接影响你写的刷新逻辑值得多提一嘴。第一个是“按需注入”。微信开发者工具里的“按需注入”Lazy Code Loading可以显著降低小程序的启动时间同时也会影响页面的加载时机。开启后小程序不会再预先加载所有页面代码而是等用户打开某个页面时才真正加载该页面的 JS。这个机制与onShow的关系是你从 A 页面跳 B 页面B 页面首次加载的代码注入发生在打开 B 的时候而不是小程序启动的时候。如果你在 B 页面的onLoad里提前初始化了一些全局数据而 A 页面的onShow依赖这些数据就可能出现 A 返回时取到的是空值。解决方法是把数据初始化放到app.js的onLaunch或者用wx.getStorageSync做兜底读取。第二个是基础库版本。如果你打开“真机调试”发现某些手机上onShow不触发先检查基础库版本再检查是不是页面被redirectTo替换而不是navigateTo压入栈。redirectTo会销毁当前页面返回时其实是回到了上一个页面而不是当前页面的onShow。用表格记一下跳转方式当前页状态返回后触发的生命周期navigateTo压入栈保留实例当前页 onShowredirectTo当前页销毁新页替换新打开页面 onLoad、onShowswitchTab关闭所有非 tab 页面目标 tab 页 onShownavigateBack当前页销毁上一页露出上一页 onShow记住这个表排查“刷新不生效”问题时会快得多。4.4 静默刷新 vs 有反馈刷新不同场景的取舍返回刷新这件事最影响体验的其实是“要不要让用户感知到刷新”。我见过很多半吊子方案就是在onShow里写wx.showLoading然后接口完成后wx.hideLoading结果每次从详情页返回时整个页面弹一个“加载中”的蒙层用户明明只是看一眼详情回来就被强制看 400 毫秒的加载动画逐渐开始烦躁。我的取舍标准就一条用户自己主动做了修改操作这时候返回列表需要明确反馈用导航栏 loading 或 toast用户只是浏览信息返回时用静默刷新就够。静默刷新时页面可以先显示旧的列表数据等新数据到了再更新。这种做法在视觉上是无缝的用户甚至感知不到数据已经刷新了但当你再次查看时数据一定是最新的。如果你一定想要视觉上的“数据更新了”效果可以在下拉刷新后加一个很轻的shake动画或者对变化的行加一个短暂高亮。微信原生没有这类动画一般得配合animation属性做工作量不大但体验提升很明显。5. 实战问题排查与避坑手册5.1 高频问题速查表把日常开发中跟onShow刷新相关的典型问题整理成一张速查表方便遇到问题直接对号入座。现象大概率原因解决方案返回上一页列表没有刷新请求写在 onLoad 里把请求迁移到 onShow首次进入就请求两次onLoad 和 onShow 都写了请求只保留 onShow 或加首次加载标志从后台切回页面出现长时间 loadingonShow 里无条件刷新接口又慢加刷新条件后台切回只刷新特定场景请求发了但页面数据没变接口缓存CDN 或服务端缓存请求加时间戳参数或 no-cache 头修改完成返回列表还是老样子上一页没有收到“需刷新”标记用 eventChannel 或全局标记位显式通知返回后列表滚动位置被重置onShow 刷新后触发列表重新渲染记录 scrollTop重新渲染后恢复位置onShow 触发但页面静止无加载提示静默刷新正常用户没感知判断是否给导航栏 loading 或 toast自定义导航栏返回无响应页面栈深度为 0navigateBack 无效写兜底逻辑跳回首页 tab5.2 排查案例请求发了但数据还是旧的有一次我在一个电商项目里给购物车列表做返回刷新代码逻辑完全正确onShow里确实发起了新的请求但返回的数据永远是旧值。抓包之后发现请求根本没到后端——接口命中了微信小程序默认的 HTTP 缓存。小程序里wx.request默认是遵守 HTTP 缓存规则的。如果你的服务端给列表接口设置了Cache-Control: max-agexxx那么在小程序端wx.request可能会直接返回缓存内容而不会真正发请求到服务器。解决方案很简单给请求地址拼一个时间戳或者随机数避开缓存wx.request({ url: ${API_BASE}/list?_t${Date.now()}, method: GET, success: (res) { // ... }, });当然更专业的做法是在服务端设置Cache-Control: no-cache或者使用ETag做协商缓存但作为前端先用时间戳招解决最直接。这个坑在请求返回刷新逻辑时特别隐蔽没有抓包工具很难发现。5.3 排查案例页面初次进入就重复请求另一个高频问题是不管是否返回页面首次进入就发了两遍请求。原因前面表格里已经提到一般就是onLoad里写了一遍请求onShow里又写了一遍。但还有种特殊情况通过navigateTo跳到子页面子页面又通过eventChannel通知父页面刷新而父页面在onShow里无条件刷新收到事件后又执行了一遍刷新结果同一时刻发出了两个相同的请求。解决这个问题的关键是“刷新的去重”。最简单的方法是用一个刷新锁Page({ data: { list: [], }, onShow() { this.safeRefresh(); }, safeRefresh() { if (this._refreshing) { return; } this._refreshing true; this.fetchList({ complete: () { this._refreshing false; }, }); }, });_refreshing是一个实例属性不是data里的数据避免它被 setData 渲染到视图里。当页面已经处于请求中时新的刷新请求直接忽略等当前请求完成后再重置锁。这个方法非常简单但在多场景并发触发onShow时非常有效。5.4 排查案例返回刷新后滚动位置被重置这个坑我踩得最熟。列表页 A 往下滑到了第 20 条点进详情页看完返回onShow触发刷新setData更新列表然后页面居然滚回了顶部。用户刚才看的内容全没了得重新滑半天。原因是setData更新列表后数据源数组的长度不变但内容更新微信小程序会基于列表key重新渲染节点当节点顺序和内容变化时滚动位置会被重置到列表顶部。两个方案可以组合使用。第一给列表项加稳定的key让小程序的 diff 算法知道哪些节点是可以复用的。配置方法是在列表的模板上写block wx:for-items{{list}} wx:keyid。第二在onShow触发热更前记录当前的scrollTop刷新完成后用wx.pageScrollTo恢复Page({ data: { scrollTop: 0, }, onPageScroll(e) { // 记录滚动位置 this._scrollTop e.scrollTop; }, onShow() { this.refreshList(); }, async refreshList() { const prevTop this._scrollTop || 0; await this.fetchList(); // 恢复滚动位置 wx.pageScrollTo({ scrollTop: prevTop, duration: 0, }); }, });需要注意的是恢复滚动位置应该在渲染完成之后所以如果你用wx.request我建议把scrollTop恢复放到success回调里setData之后的wx.nextTick中保证列表已经重新渲染出了。否则你恢复了一个空页面的高度效果依然是回顶部。5.5 登录态失效与 token 过期onShow 刷新时记得做鉴权最后聊一个容易被忽略但很重要的点onShow触发时用户可能早就登录失效了。比如用户打开了详情页在详情页停留了很久切换到别的 App 或者过了很长时间回来触发onShow刷新列表结果所有请求全部返回 401。这时候最差的做法是弹一个错误 toast最好的做法是静默用wx.login换新 token然后再重新拉数据。提到wx.login它是小程序登录态的核心通过wx.login()获取临时code把code交换到后端换取openid和自定义登录态比如 token再把这个 token 存起来供后续接口使用。在onShow刷新时如果接口返回 401可以重新走一次wx.login换 code 的流程刷新 token 后重放请求。onShow() { this.requestWithAuth(/list); }, requestWithAuth(url) { const token wx.getStorageSync(token); wx.request({ url, method: GET, header: { Authorization: Bearer ${token} }, success: (res) { if (res.statusCode 401) { wx.login({ success: (loginRes) { wx.request({ url: https://api.example.com/auth/login, method: POST, data: { code: loginRes.code }, success: (authRes) { wx.setStorageSync(token, authRes.data.token); this.requestWithAuth(url); }, }); }, }); } else { this.setData({ list: res.data.list }); } }, }); }老实说这段代码在业务里通常会被封装得更好看比如用 Promise 包装成一个request公共函数所有接口都自动带鉴权。但理解“返回刷新的请求也要走鉴权逻辑”这个意识比实现细节更重要。很多人只关心刷新忘了 token 是否有效结果返回时看到一片错误红屏。最后分享一点个人体会从我经手的小程序项目来看onShow刷新的坑从来都不在“不会写”而在“什么时候该写、什么时候不该写”。如果我现在接手一个新项目凡是列表页、详情页、表单页我会直接在页面里启用onShow 刷新开关 事件通知的组合方案。列表页默认refresherEnabled为 true但从详情页返回时只有详情页主动emit(refreshList)才真正拉接口从后台切回时默认不刷新除非是超长超时场景才特殊处理。小程序的页面生命周期设计得本来就挺简洁onShow这个函数就是微信专门留给页面“重新展示”时的机会窗口。你不好好利用它它就只是一个平常的回调你利用好了整个应用的数据同步、刷新体验都会顺畅很多。希望这篇能帮你少踩几个坑多省几根头发。
返回列表