☰
微信小程序运行机制与性能优化全解析:从双线程模型到setData与分包实践
2026/11/2 13:25:33 网站建设 项目流程

做微信小程序这七八年,被问得最多的问题从来不是"这个页面怎么写",而是"为什么开发者工具里丝般顺滑,一到真机就卡成PPT"、"为什么这个接口在模拟器上返回正常,用户手机上却一片空白"。这类问题的答案,八成不在业务代码里,而在于你对小程序这套运行机制的理解深度。微信小程序看起来就是写写 WXML、调调 wx.xxx,上手门槛低得离谱,但它底层是一套逻辑层与视图层分离、跨线程通信、包体受限、生命周期严格的运行时环境,很多在 Web 上理所当然的做法,搬进来就是坑。这篇内容我想把微信小程序的知识体系从头捋一遍:运行骨架长什么样、项目怎么搭才不会后期返工、setData 为什么是性能分水岭、组件化和分包怎么做才不别扭、以及那些只有真机上才会冒出来的问题该怎么排查。不管你是刚接触小程序两三周的新手,还是已经能独立交付项目但总觉得代码越写越乱的开发者,应该都能从里面找到可以直接抄走的东西。

1. 先把小程序的运行骨架搞清楚

1.1 双线程模型到底在解决什么问题

很多人写了大半年小程序,都不知道自己的 JS 代码和页面渲染根本不在同一个线程里跑。微信小程序采用的是双线程模型:逻辑层(AppService)跑在一个独立的 JavaScript 引擎里,负责执行你的业务 JS、处理数据、发网络请求;视图层(WebView)负责渲染 WXML 和 WXSS,两个线程之间通过客户端原生层做消息中转。这个设计不是随便定的,核心目的是隔离与管控——页面里的 JS 拿不到真实的 DOM,也就没法做任意 DOM 操作,渲染结果完全由数据驱动生成,这样既保证了页面结构可控,也让一套代码能稳定跑在不同版本的宿主环境上。

理解这一点,很多"怪异现象"就顺了。比如你在 JS 里document.getElementById一定报错,因为根本没有 document;比如你在 WXML 里写了一个复杂表达式,页面上数据变了但界面没变,因为表达式只在数据变更触发重新渲染时才求值;再比如setData为什么必须显式调用而不是像 Vue 那样自动响应,就是因为跨线程通信成本太高,框架不敢帮你自动侦测——它必须由你明确告诉它"这块数据变了,请同步过去"。

我个人把这个模型类比成"两个隔着墙的同事"。逻辑层是产品经理,视图层是设计师,中间隔着一堵墙(Native 层)。产品经理改需求,必须写纸条从墙缝里递过去,设计师才能改稿。纸条一次能写多少字是有限的,递得太频繁,两边都累。所以所有关于小程序性能的优化,归根到底就是在做一件事:减少递纸条的次数和纸条的长度。

1.2 视图层与逻辑层的通信代价

通信代价体现在哪?当你在逻辑层调用setData时,数据会先被序列化成一个 JSON 字符串,通过桥接层传到视图层,视图层再反序列化、做 diff、更新真实节点。这一整条链路的耗时和数据体积直接相关,而不是和"改了几个字段"相关。也就是说,你setData({ a: 1 })和你setData({ obj: 一个 500KB 的对象 }),前者的开销几乎可以忽略,后者的开销可能直接造成几十毫秒的卡顿,在低端安卓机上甚至肉眼可见。

这也是为什么官方建议单次setData的数据量控制在一个比较小的量级(文档里给的经验值是不宜超过 1MB,实际工程中我建议控制在 100KB 以内更保险),并且只传变化的部分。举个最常见的反面例子:一个商品列表页,用户点了某个商品的收藏按钮,很多人会写成this.setData({ list: newList })——把整份列表重新传一遍。如果列表有 200 条数据、每条 20 个字段,这一下就是几百 KB 的序列化开销,用户点一次收藏,页面卡一下。正确做法是用路径写法:

// 只更新第 index 条数据的 collected 字段,其余数据不动 this.setData({ [`list[${index}].collected`]: true });

框架会把这种路径语法解析成"只更新这个位置",传输的数据量小到几乎可以忽略。这个技巧看起来不起眼,但在长列表场景下,它带来的体感差异是数量级的。

注意:路径写法里的 key 必须用方括号语法动态拼接,写成字符串'list[index].collected'是无效的,框架不会帮你解析变量,只会去找一个名叫index的字段。

1.3 项目目录与配置文件的职责边界

小程序的工程结构是"约定大于配置"的典型。一个页面由四个同名文件组成:.wxml(结构)、.wxss(样式)、.js(逻辑)、.json(页面配置)。这套四件套的划分很清晰,但新手最容易犯的错是把配置散着写——一会儿在app.json里加,一会儿在页面.json里加,最后自己都记不住哪个配置生效。

我的做法是划一条硬边界:全局性的、影响整个应用行为的配置放app.json(页面路由注册、窗口表现、tabBar、分包、权限声明);只影响单个页面的配置放页面.json(导航栏标题、是否开启下拉刷新、是否允许滚动)。页面.json的优先级高于app.json的window配置,所以同一个标题你在两处都写了,页面级的会覆盖全局的,这个覆盖关系搞不清楚,就会出现"我明明改了标题怎么没变"的困惑。

另外一个常被忽略的点是sitemap.json。它控制小程序页面是否允许被索引,很多团队直接复制模板了事,其实如果项目里有敏感数据页(比如订单详情、个人中心),应该明确配置不允许索引。这不是性能问题,是意识问题。

2. 从零搭一个能跑的项目骨架

2.1 目录结构与最小可运行单元

我不太赞成新手直接拿复杂的脚手架起步,因为你会不知道自己写的代码到底被谁加载了。我的建议是先用最朴素的目录结构跑通一个页面,把链路走通,再往上叠工程化。一个干净可维护的目录大概长这样:

├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ │ │ ├── index.wxml │ │ ├── index.wxss │ │ ├── index.js │ │ └── index.json │ └── detail/ ├── components/ │ └── goods-card/ ├── utils/ │ ├── request.js │ └── format.js └── static/

几个我认为必须坚持的约定:页面只做编排,不做实现——页面 JS 里应该只有取数、组装、跳转,具体展示逻辑丢给组件;utils 目录只放纯函数和基础能力封装,不要在里面写业务;static 目录按业务模块分子目录,否则三个月后你会面对一个几百个文件的图片文件夹发呆。这些约定听起来是小事,但它们决定了项目能不能活过第三个迭代。

最小可运行单元的判定标准很简单:app.json的pages数组里注册了页面路径,且该路径下四个文件存在(.json可以省略但建议保留)。少一个文件,编译就直接挂,报错信息有时候不太友好,容易让人误以为是路径拼错了。

2.2 app.json 里的那些关键配置

app.json里有几个配置项,我认为是必须理解到位而不是抄模板的。第一个是pages数组的顺序——数组第一项就是启动页,改顺序会直接改变用户打开小程序看到的第一个页面,这个在测试环境切首页时特别容易踩。第二个是window里的navigationStyle,设成custom之后原生导航栏消失,你可以自己画一个,但代价是状态栏高度、胶囊按钮位置都得自己算,没有现成的高度值,需要用wx.getSystemInfoSync()拿到状态栏高度再配合固定的胶囊区域做布局。

第三个是tabBar,这里最容易出问题的是图标路径必须用本地文件,不能用网络地址,而且尺寸有建议范围。我见过不止一个项目因为 tabBar 图标尺寸不规范,在一些机型上被拉伸得很难看。

{ "pages": ["pages/index/index", "pages/detail/detail"], "window": { "navigationBarTitleText": "示例应用", "backgroundTextStyle": "dark", "navigationBarBackgroundColor": "#ffffff", "navigationBarTextStyle": "black" }, "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home-active.png" } ] }, "permission": { "scope.userLocation": { "desc": "用于展示附近的门店" } } }

提示:permission里的 desc 不是随便写的说明文字,它会直接出现在用户授权弹窗里。写得含糊(比如"需要获取您的位置")会显著降低授权通过率,写清楚用途("用于展示离你最近的门店")通过率会高很多。

2.3 生命周期全景:App、Page、Component 三套

小程序有三套生命周期,新手最容易混。App 级的onLaunch在冷启动时执行一次,onShow每次从后台切回前台都会执行,注意这两个不是同一件事——很多人把登录逻辑写在onLaunch里,结果用户从后台切回来发现登录态没刷新,就是因为没在onShow里做二次校验。

Page 级的onLoad只在页面创建时执行一次,接收路由参数;onShow每次页面显示都执行(包括从下级页面返回);onReady表示首次渲染完成,此时才能拿到节点信息。这个顺序很关键:如果你在onLoad里调wx.createSelectorQuery去量某个元素的高度,大概率量到 0,因为渲染还没完成,必须挪到onReady里,或者用setTimeout兜一下(后者不推荐,是碰运气)。

Component 级是另一套:created(此时还不能 setData)、attached(可以开始操作)、ready(布局完成)、detached(移除)。自定义组件里如果同时用了 Page 的写法,会发现onLoad根本不触发——因为它不是页面。我见过有人把页面代码抽成组件后忘记改生命周期,排查了半天。

我的经验是:数据初始化放onLoad/attached,依赖布局的计算放onReady/ready,每次可见都要做的刷新放onShow,资源清理放onUnload/detached。这四条守住,八成生命周期相关的 bug 就没了。

2.4 事件绑定与数据流的两种写法

WXML 的事件绑定分冒泡(bindtap)和不冒泡(catchtap)两种。这个区别在列表项里点内部按钮时特别重要:外层容器绑了bindtap跳详情,内部收藏按钮如果也用bindtap,点收藏会连带触发跳转,必须用catchtap阻止冒泡。这个坑几乎每个小程序开发者都踩过一次。

数据流方面,小程序没有 Vue 那种双向绑定(model:value是后来加的语法糖,但只对简单场景有效)。常规做法是绑bindinput拿值再setData。这里有个性能细节:输入框每敲一个字就 setData 一次,如果这个页面还有其他复杂渲染,会明显掉帧。比较稳的做法是只把需要实时展示的值放 data,其余的值存在组件实例的普通属性上,提交时再读:

Page({ data: { keyword: '' }, onInput(e) { // 只同步需要用于展示的部分 this.setData({ keyword: e.detail.value }); // 完整值存在实例上,避免进 data 走通信 this._rawKeyword = e.detail.value; }, onSubmit() { console.log(this._rawKeyword); } });

3. 数据层与 setData:性能分水岭

3.1 setData 的真实成本与四条铁律

如果把小程序性能问题排个序,setData滥用的占比绝对第一。前面说过它的成本来自数据序列化与跨线程传输,具体到工程实践,我总结了四条铁律,基本能覆盖 90% 的场景。

第一条:能拼路径就不传整对象。前面已经举例,this.setData({ [list[${i}].name]: val })远优于重建数组。第二条:不在onPageScroll、onReachBottom这类高频回调里做重量级 setData。滚动回调一秒能触发几十次,每次都 setData,页面必卡。如果确实需要根据滚动位置变化样式,用 WXS 在视图层直接算,或者用 CSS 的position: sticky、IntersectionObserver代替。第三条:setData里不要塞 undefined。这个值会被序列化时丢掉,导致你以为清空了某个字段,实际上视图层还是旧值。要清空就传null或空字符串。第四条:不要在setData的回调里做耗时逻辑。回调确实表示视图更新完成,但那个时机的线程状态并不适合做重活。

实操心得:调试 setData 开销有个很土但很好用的办法——在setData前后各打一个时间戳,把差值打到日志里。真机调试试几轮,你就会对自己的数据模型有全新认识,通常能揪出两三个"传了但根本没用到"的大字段。

3.2 长列表与分页加载的实操方案

长列表是小程序里最考验功力的场景。列表几百条以内,scroll-view加普通渲染完全够用;上千条就开始明显卡顿;上万条基本必须上虚拟列表。

我的一般策略是分三档:200 条以内,直接全量渲染,配合lazy-load让图片延迟加载;200 到 1000 条,分页加载,每次setData用concat追加而不是全量替换,同时把不展示的复杂组件用wx:if控制;1000 条以上,上虚拟列表——只渲染视口内可见的十几条,滚动时动态替换数据。虚拟列表自己实现不难但坑不少(滚动位置校准、高度不固定时的测量),我更倾向用成熟的方案或者干脆做成"分页 + 触底加载",别硬刚。

分页加载有个经典 bug:用户快速上拉,触发了三次加载,结果第二页数据重复插入了。解决办法是加一个loading锁:

onReachBottom() { if (this._loading || !this._hasMore) return; this._loading = true; fetchList({ page: this._page }).then(res => { this.setData({ list: this.data.list.concat(res.list) }); this._page += 1; this._hasMore = res.hasMore; }).finally(() => { this._loading = false; }); }

注意锁变量我放在实例上(this._loading)而不是 data 里,因为它不需要驱动视图,放 data 里反而多一次通信。

3.3 网络请求封装与错误处理

wx.request是回调式的,直接铺在业务代码里会写出一堆嵌套。我的做法是包一层返回 Promise 的request,统一处理域名前缀、登录态、错误提示、超时重试。这里面有几个细节值得说。

并发数方面,小程序对wx.request有并发上限(同时发起的请求数受限),超过会排队。如果一个页面初始化要打七八个接口,用户感知就是"转圈很久"。我的做法是能合的接口合并,不能合的用Promise.all并发但要给每个请求设超时兜底,避免某一个慢接口拖垮整个页面。

登录态刷新是最容易出问题的地方。常规做法是请求前检查本地 token,过期就先调登录接口换新,再重放原请求。这里必须加一个"刷新中"的队列,否则十个并发请求同时发现 token 过期,会触发十次登录,后端直接把你拉黑。

// 简化的重试 + 刷新队列思路 let refreshing = null; function request(options) { return new Promise((resolve, reject) => { wx.request({ ...options, url: BASE_URL + options.url, header: { Authorization: `Bearer ${getToken()}` }, success(res) { if (res.statusCode === 401) { refreshing = refreshing || refreshToken().finally(() => { refreshing = null; }); refreshing.then(() => request(options).then(resolve, reject)); return; } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data); } else { reject(res); } }, fail: reject }); }); }

注意:小程序的网络请求域名必须在后台配置过白名单,且是 HTTPS。开发者工具里可以勾选"不校验合法域名"绕过,但这只是本地调试的便利,上线必然失败。我见过上线前一天才发现接口域名没配的团队,那种慌乱场面不想再看到第二次。

3.4 本地存储与缓存策略

wx.setStorageSync用起来太方便了,导致很多人把它当数据库使。实际上它有容量限制(单个 key 和总量都有上限),而且是同步阻塞的,在主线程里读写大对象会卡住界面。我的原则是:只存"小、稳、读得频繁"的东西——登录态、用户偏好、最近一次的位置信息。列表数据、图片、大段文本一律不往本地存储里塞。

缓存策略上,我更倾向"内存缓存 + 时效标记"的组合:把列表数据挂在页面实例的属性上,同时在存储里只记一个时间戳,下次进页面判断时间戳是否超过设定的有效期(比如 5 分钟),超了才重新拉。这样既避免了每次进页面都白屏加载,也不会因为本地存了过期数据导致展示错误。

4. 组件化与工程化落地

4.1 自定义组件的属性、事件与插槽

组件化的第一步是想清楚"什么是组件"。我的判断标准是:同一段 UI 结构在两个以上页面出现,或者它有独立的状态和交互,就抽组件。抽得太碎会导致页面全是标签、可读性下降;抽得太粗又起不到复用作用。

自定义组件的核心是三样东西:properties(外部传入)、events(向外通知)、slot(内容分发)。属性定义里有个容易忽略的点是observer——属性变化时触发,但它不能在里面 setData 修改自身属性,会死循环。需要根据属性派生数据时,用observers监听多个字段,或者干脆在attached里做一次初始化。

Component({ properties: { goods: { type: Object, value: {} }, showPrice: { type: Boolean, value: true } }, data: { displayPrice: '' }, observers: { 'goods.price, showPrice'(price, showPrice) { this.setData({ displayPrice: showPrice ? formatPrice(price) : '' }); } }, methods: { onTapCard() { this.triggerEvent('select', { id: this.data.goods.id }); } } });

插槽方面,默认只能有一个 slot,要用多个必须开multipleSlots: true并在 WXML 里用name区分。这个配置写在组件的.js的options里,忘了写就只有第一个 slot 生效,症状是"我明明放了两个插槽,第二个内容凭空消失"。

4.2 behaviors 复用逻辑

页面之间、组件之间总有那么些重复逻辑:埋点上报、登录校验、分页加载。这些不适合抽成组件(它们没有 UI),用behaviors更合适。一个 behavior 就是一个可复用的代码片段,里面可以定义 data、methods、生命周期,被引入后会自动合并。

// behaviors/pagination.js module.exports = Behavior({ data: { list: [], hasMore: true }, methods: { resetList() { this.setData({ list: [], hasMore: true }); this._page = 1; }, appendList(items, hasMore) { this.setData({ list: this.data.list.concat(items), hasMore }); this._page = (this._page || 1) + 1; } } });

用的时候注意字段冲突:behavior 里的 data 和组件自身的 data 同名时,组件自身的优先;生命周期则是"behavior 先执行、组件后执行"。这个顺序在需要初始化顺序保证的场景下很重要,搞反了就会出现"我在 behavior 里读到的字段还是空的"。

4.3 分包与首屏优化

包体积是小程序绕不过去的坎。主包有大小限制,超了直接无法上传。分包的意义就在于把非首屏必需的页面和资源挪出去,让用户先看到首页,用到再下载。

分包的设计原则是按业务路径切,不要按文件类型切。比如一个电商类小程序,首页 + 商品详情 + 下单是主链路,放主包;个人中心、历史订单、售后流程是低频路径,放分包。我见过按"图片放一个包、页面放一个包"切的,结果进详情页要同时下载两个分包,反而更慢。

配置上,app.json里声明subpackages,配合preloadRule做预下载:用户停留在首页时,后台悄悄下载详情页所在的分包,等他点进去就是秒开。这个预下载策略要克制,别把所有包都预下载,会白白消耗用户流量。

{ "subpackages": [ { "root": "packageOrder", "pages": ["order/list", "order/detail"] } ], "preloadRule": { "pages/index/index": { "network": "wifi", "packages": ["packageOrder"] } } }

提示:preloadRule的 network 可选all或wifi。移动流量下预下载大分包体验很差,建议至少设成 wifi,除非那个分包很小且转化路径极短。

4.4 npm 与构建配置

小程序支持 npm 包,但需要"构建 npm"这一步,不是装了就能用。这一步的本质是把node_modules里被用到的代码复制并转换到miniprogram_npm目录。常见问题是构建完没生效——八成是因为project.config.json里的miniprogramRoot和packNpmManually配错了,导致构建产物放错位置。

我的建议是:能用原生写的就别引包。小程序环境不是完整 Node 环境,很多库依赖window、document、Buffer,装进来也跑不起来。真正值得引的比较有限:日期处理、工具函数、图表(有专门适配的版本)。UI 组件库要看包体积,有些库全量引入能吃掉主包一大半配额,务必按需引入。

5. 常见问题排查实录

5.1 页面栈与跳转失效

wx.navigateTo最多保留十层页面栈,超过就跳不动了,而且它不能跳转到 tabBar 页面。这两个限制造成的报错经常被误判成"路径写错了"。

排查思路很简单,先判断目标页面类型:如果是 tabBar 页,必须用wx.switchTab;如果是需要重置整个流程(比如下单完成后回首页),用wx.reLaunch;如果是替换当前页(比如登录页登录成功后去首页),用wx.redirectTo。层级接近上限时,可以考虑redirectTo替代,或者检查是不是有循环跳转——我遇到过一次是详情页里的"相关推荐"点了又进详情页,用户划着划着就跳不动了。

跳转方式是否保留当前页能否跳 tabBar典型场景
navigateTo保留否列表进详情
redirectTo不保留否登录后进首页
switchTab清空非 tab 栈是底部导航切换
reLaunch全部关闭是流程结束回首页
navigateBack关闭当前页—返回上一级

5.2 样式与布局踩坑

小程序里最让人抓狂的样式问题,排名第一的是组件样式隔离。你在页面 wxss 里写的选择器,默认影响不到自定义组件内部;反过来组件里的样式也不会污染外面。这个设计本身是好的,但新手不知道,就会出现"我在页面里给组件里的按钮加了样式,怎么都不生效"。解决办法有三种:组件开styleIsolation: 'apply-shared'、用externalClasses暴露类名、或者直接在组件内部写好样式。我一般选第三种,最干净。

排名第二的是scroll-view的高度问题。它必须有一个确定的高度才能滚动,写height: 100%往往不生效,因为父级没有确定高度。我的做法是用height: 100vh配合calc(比如calc(100vh - 88rpx)减去头部),或者在onReady里用createSelectorQuery量出可用高度再 setData。

排名第三是rpx的误用。rpx是按屏幕宽度等比换算的,在宽屏设备(比如平板、折叠屏)上,750rpx对应的物理尺寸会变得很大,字号会显得夸张。我的经验是:布局尺寸用 rpx,字号和需要精确控制的间距用 px,这样跨设备观感更稳定。

5.3 兼容性问题:只在真机上出现的那些坑

第一个大坑是日期解析。new Date('2024-01-01 10:00:00')这种带横杠加空格的格式,在 iOS 上会返回Invalid Date,安卓和开发者工具都正常。原因是 iOS 的解析器更严格,只认 ISO 格式。解决办法是把横杠换成斜杠:new Date(str.replace(/-/g, '/')),或者用/分隔手动拼。这个 bug 的隐蔽之处在于它只在 iOS 真机上炸,测试覆盖率不够的话,上线后才会被用户投诉。

第二个是position: fixed在部分安卓机上跟随滚动抖动的老问题,通常通过给页面容器设overflow: hidden或者改用绝对定位加高度计算来规避。第三个是输入框在键盘弹起时页面不自动上推,需要监听bindfocus拿键盘高度,手动调整容器padding-bottom。

第四个是图片的mode属性。默认是scaleToFill,会把图片拉伸变形,很多人忘了改。列表里的缩略图建议用aspectFill,头像建议外面套一层圆角容器。

5.4 问题速查表

现象大概率原因处理方式
页面白屏无报错路由未注册或路径大小写不符检查 app.json 的 pages
数据改了视图没变直接改了 data 对象未 setData用路径写法 setData
组件样式不生效样式隔离组件内部写样式或开 apply-shared
iOS 日期显示 NaN日期字符串格式不兼容横杠换斜杠
点击列表项误触发跳转事件冒泡内部按钮用 catchtap
上传包体超限主包塞了非必需资源拆分包、图片走 CDN
真机请求失败域名未配白名单后台配置合法域名
下拉刷新不触发页面 json 未开启enablePullDownRefresh: true

6. 几个我踩过之后才想明白的点

第一个是关于"数据模型"。刚开始写小程序时,我习惯把接口返回的数据结构原封不动塞进 data,觉得省事。直到有一次接口返回的对象里带了一个巨大的富文本字段,列表页每次 setData 都卡顿,才发现问题。后来我养成一个习惯:接口数据和视图数据分开,在onLoad里做一次映射,只把视图真正需要的字段放进 data,多余的字段挂在实例属性上备用。这一个动作,往往能砍掉一半以上的传输量。

第二个是关于调试。开发者工具和真机是两套环境,这话说起来轻巧,但真正建立"真机必测"的条件反射是要付学费的。我现在的习惯是:任何涉及日期、文件、相机、地理位置、支付的功能,写完立刻扫码真机验证一遍,不等联调。十分钟的验证能省下一整天的返工。

第三个是关于基础库版本。新特性基本都绑定了最低基础库版本,用了observers、MultipleSlots、分包异步化这些能力,就得在后台把最低版本设好,否则老版本用户看到的就是白屏或者功能缺失。上线前看一眼"基础库版本分布"这个数据,能帮你判断要不要为老版本做降级。

第四个是关于代码量。小程序单个文件写太长,编辑器里的可读性会断崖式下降,尤其是 WXML,样式和结构混在一起超过两百行就开始难受。我的做法是页面 WXML 控制在 150 行以内,超了就往下拆组件;JS 里的业务方法超过 30 行就抽到 utils 里。这个限制不是为了好看,是为了三个月后你自己还能看懂。

最后再说一个容易被忽略的细节:小程序的onShow在页面首次加载时也会触发一次,顺序在onLoad之后、onReady之前。所以如果你在onShow里做了数据刷新,onLoad里的取数逻辑就要考虑会不会重复请求。我一般的处理方式是给刷新方法加一个"是否首次"的判断标记,首次交给onLoad,后续交给onShow,两边不打架。这种小地方没人会在文档里特意提醒你,但踩过一次之后,它就会变成你代码里的肌肉记忆。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询