← 全部材料 小程序
MINI PROGRAM · 14 个高频点

小程序不是小号的网页

它的坑几乎都来自同一个前提:没有 DOM,逻辑和渲染在两个线程。理解了这条,setData 为什么慢、为什么不能直接改 data、为什么原生组件的层级那么怪,都不用单独记了。

01先立住双线程模型所有性能和安全问题都能从这里推出来,别单独背结论。
02跨端框架看产物问"编译期还是运行时",答案就定了性能、能力、包体积三个取舍。
03区分三套生命周期页面、组件、应用各一套,答错顺序就是没写过。
1

双线程架构:为什么 setData 是性能关键★

一次 setData,跨了两条线程
小程序把逻辑和渲染拆到两个线程,中间隔着 Native 层转发。所以任何一次数据更新都不是"改个变量",而是序列化 + 跨线程通信 + 反序列化 + diff + 更新 DOM。setData 慢,慢在这条链路的长度,不在数据本身。
◐ 可交互:数据量一变,setData 的成本差多少
列表条数100
每条字节数80
拖动滑块,看三种写法的差距怎么被放大。数值是数量级示意,不是实测。
一次更新要多久(毫秒)
整包 setData ×3 次0
只传变化路径 ×3 次0
整包但合并成 1 次0

逻辑层(JSCore / V8)

  • 跑你的 JS:Page、App、业务逻辑
  • 没有 DOM,没有 window / document
  • 没法直接操作视图

渲染层(WebView)

  • 跑 WXML + WXSS,本质是一个被裁剪的 WebView
  • 负责最终绘制
  • 不能执行你的业务 JS
setData(obj)逻辑层收到调用
→
序列化JSON 化后通过 Native 转发
→
渲染层收到反序列化,合并进 data
→
diff + patch对比新旧 WXML 树,算出最小改动
→
绘制改真实 DOM,屏幕更新
面试怎么答小程序是双线程的,逻辑层跑在 JSCore 或 V8 里,没有 DOM,渲染层是 WebView。两边的数据不共享,setData 实际上是序列化后跨线程发到渲染层,再 diff WXML 树去更新。所以它的成本跟数据量强相关,这也是为什么优化的核心是"少传、传得小、合并传"。
2

WXML 编译成什么

它不是 HTML,也不是模板引擎
WXML 在构建期被编译成一段 JS 函数,运行时靠它生成虚拟节点树。所以 wx:if 这类指令最终是 if 分支,{{ }} 是取值表达式,跟你写的模板字符串是两套东西。
编译产物大致长这样(简化)function render(context) { return { tag: 'view', attr: { class: 'card' }, children: [ context.loading ? { tag: 'text', children: ['加载中'] } : { tag: 'block', children: [ { tag: 'text', attr: { class: 'title' }, children: [context.item.title] }, ...context.tags.map(item => ({ tag: 'text', children: [item] })) ] } ] }; }
面试怎么答WXML 在构建时被编译成生成虚拟节点的函数,运行时执行它得到一棵树,再跟上次的树 diff,最后只把差异应用到渲染层的真实 DOM 上。所以 wx:if 是编译出来的分支,wx:for 是 map,wx:key 决定 diff 时节点能不能复用。
3

setData 的三条优化★

同一份数据,能不能只传变化的那一小块
优化就三件事:只传变化的部分(用路径写法)、合并调用(别在循环里 setData)、控制频率(滚动、输入这类高频事件要节流)。三条都不涉及"减少数据量"以外的技巧,因为瓶颈就是传输量。
面试怎么答setData 的成本主要在跨线程传输和反序列化,所以优化分三层:能用路径写法只传变化项就别传整个对象;能把多次合并成一次就别在循环里调;高频事件用节流压频率。如果列表很长,那就上虚拟列表,让传输量跟总条数无关。
4

生命周期的执行顺序★

页面、组件、应用三套,顺序容易答反
面试常让你说"从 A 页跳到 B 页,两边的生命周期怎么走"。记住一句话:先走完当前页的 onHide,再走新页的 onLoad → onShow → onReady。返回时反过来,且旧页不会重新 onLoad。
A.onHide跳转前,当前页隐藏
→
B.onLoad新页加载,拿到路由参数
→
B.onShow新页显示
→
B.onReady新页首次渲染完成,可调 wx.createSelectorQuery
几个容易记混的对应关系onLoad 只执行一次,等于 Vue 的 created + 拿参数 onShow 每次显示都执行(含从后台切回、从 B 返回) onReady 首次渲染完成,只一次;等于 Vue 的 mounted onHide 跳到别页 / 切后台 onUnload 页面被销毁(redirectTo、navigateBack 返回时) 组件 attached ≈ created,但此时还不能拿节点 组件 ready ≈ mounted,可以拿节点
面试怎么答打开 B 页时,A 先 onHide,然后 B 依次 onLoad、onShow、onReady。从 B 返回 A 时,B 走 onUnload,A 只走 onShow 不走 onLoad。所以依赖页面显示时刷新的逻辑要放在 onShow 里,放在 onLoad 里返回后不会执行。
5

自定义组件与样式隔离

为什么我的样式串到组件里了
自定义组件的样式默认是隔离的:外面的 class 选择器进不去,里面的也出不来。但 app.wxss 里的全局样式能影响组件,这是最常被误解的一点。
三种样式隔离模式isolated 默认。内外互不影响(但 app.wxss 的标签选择器仍可能命中) apply-shared 外部样式可以影响组件内部,反之不行 shared 双向影响,两边互通
面试怎么答自定义组件默认是 isolated 样式隔离,外部选择器进不去,组件内部样式也不会外泄,但 app.wxss 的全局样式仍然可能命中组件内的标签。需要父级样式进入组件就设 apply-shared,或者用 externalClasses 暴露外部类名。逻辑复用抽 behavior,跟 Vue 的 mixin 同源,注意命名冲突。
6

路由与页面栈

十层上限,和五种跳转的区别
小程序没有"路由表",页面栈就是一个最多 10 层的栈。navigateTo 压栈、navigateBack 出栈,redirectTo 替换栈顶,switchTab 清空栈只留 tab 页。选错方法就会出现"返回不了"或"页面越点越深"。
五种跳转的行为差异wx.navigateTo 压栈,可带参数,不能跳 tabBar 页;超过 10 层失败 wx.redirectTo 替换当前页(出栈再入栈),当前页 onUnload wx.switchTab 只能跳 tabBar 页,且会清空其它页面栈 wx.reLaunch 关闭所有页面,只留目标页 wx.navigateBack 出栈,可指定 delta 层数
参数与通信传参:url 上带 query,目标页 onLoad(options) 取 大数据:不要塞 url(长度和编码都有限制),用全局变量或 EventChannel EventChannel:navigateTo 的 events + success 里拿 eventChannel,适合"从子页回传结果" 回流:getCurrentPages() 拿页面栈,直接调上一页的方法(能跑,但耦合高,慎用)
面试怎么答小程序用页面栈管理路由,最多 10 层。打开新页用 navigateTo 压栈,替换当前页用 redirectTo,跳 tab 页必须用 switchTab 并且会清掉栈,需要重新从首页开始用 reLaunch。参数走 url query,参数多或对象复杂就改用全局状态或 EventChannel,别硬塞 url。
7

分包加载★

为什么你的主包总是超 2MB
主包上限 2MB,所有包合计 20MB,单个分包也是 2MB。分包的意义不是"能装更多",而是首启只下载主包。所以判断标准很清晰:首屏用不到的一律挪进分包。
面试怎么答主包限 2MB、总包 20MB,所以要按"首屏是否需要"来分。分包之间不能互相引用,公共依赖只能放主包或独立分包。跳转白屏用 preloadRule 预下载解决,独立分包用于分享直达的活动页,能让首屏不等主包。
8

登录流程:为什么前端拿不到 openid★

这里答错,安全题就废了
整个流程的关键是 code 只能用一次、session_key 绝不能下发到前端。wx.login 拿到的 code 交给自己的后端,后端用 appid + secret 换 openid,再把自定义的 token 返回给前端。任何"前端直接换 openid"的方案都是把 secret 暴露在客户端。
wx.login()前端拿到临时 code
→
发给自己的后端code 上传,不带任何密钥
→
code2session后端用 appid + secret 换 openid / session_key
→
签发 token后端生成自己的登录态
→
前端存 token后续请求带在 header 里
面试怎么答前端 wx.login 拿到临时 code 传给自己的后端,后端用 appid 和 secret 调 code2session 换到 openid 和 session_key,然后签发自己的 token 返回。session_key 绝对不能下发到前端,因为它能解密手机号等敏感数据。code 是一次性的,所以必须换成长效 token。
9

uni-app 怎么做到一套代码多端★

编译期转换,不是运行时模拟
uni-app 走的是编译期路线:同一份 Vue 代码,编译到小程序时产出 WXML/WXSS,编译到 H5 时产出普通 Vue 应用,编译到 App 时走原生渲染。所以它的性能接近原生小程序,因为产物就是原生语法,运行时没有额外的中间层。
条件编译的几个平台标识MP-WEIXIN 微信小程序 MP-ALIPAY 支付宝小程序 MP 所有小程序 H5 浏览器 APP-PLUS App(iOS / Android) APP-PLUS-NVUE App 的 nvue 原生渲染
面试怎么答uni-app 是编译期的方案,同一份 Vue 代码分别编译成小程序的 WXML/WXSS、H5 的普通 Vue 应用、App 的原生渲染。因为产物就是各端原生语法,运行时没有额外中间层,所以性能比运行时方案好。跨端差异用条件编译处理,它是编译期删代码,不会增加包体积。
10

uni-app 的生命周期对应关系

Vue 的钩子和小程序的钩子同时存在
uni-app 里两套生命周期并存:Vue 的(created / mounted)和小程序页面的(onLoad / onShow / onReady)。不要混着用,尤其别把"显示时刷新"写进 mounted —— 它只执行一次。
两套钩子的执行顺序(页面首次进入)beforeCreate → created onLoad(options) ← 小程序钩子,拿路由参数 onShow beforeMount → mounted onReady ← 渲染完成,可查节点 再次显示(从别的页返回): onShow ← 只有这个会走
面试怎么答uni-app 同时有小程序页面生命周期和 Vue 生命周期。首次进入的顺序是 created、onLoad、onShow、mounted、onReady。要拿路由参数用 onLoad,要在页面重新显示时刷新用 onShow,因为从别的页面返回只触发 onShow,mounted 和 onLoad 都不会再执行。
11

Taro 的编译原理:三条路线★

编译期还是运行时,决定了一切取舍
Taro 1/2 是编译期:把 JSX 直接转成 WXML,产物是纯小程序代码,性能好但 React 能力受限。Taro 3 之后改成运行时:在小程序里跑一份 React,用它维护一棵 DOM 树,再映射到小程序的节点上。好处是 React 生态全能用,代价是体积和一层开销。

编译期(Taro 1/2)

  • JSX 静态转成 WXML,产物接近原生
  • 性能最好,包体最小
  • 不能用完整 React:hooks、context 受限
  • 动态逻辑难表达,很多写法要绕

运行时(Taro 3+)

  • 小程序里跑真正的 React 运行时
  • 维护一棵 Taro DOM 树,再同步到视图层
  • React 的一切都能用:hooks、context、生态库
  • 代价:包体更大,更新多一层映射
Taro 3 的运行时到底做了什么你的 React 代码 → React Reconciler → Taro DOM(模拟的节点树) ↓ 转成 setData 的增量 → 小程序视图层 也就是说:React 的 diff 发生在 Taro DOM 上, 最后还是要通过 setData 到原生节点。所以 setData 的优化依然重要。
面试怎么答Taro 早期是编译期方案,JSX 直接转 WXML,性能好但 React 能力受限。Taro 3 改成运行时,在小程序里跑真实的 React,用 Taro DOM 模拟节点树,再把增量通过 setData 同步到视图层,所以能用完整的 React 生态,代价是包体积和一层映射开销。
12

原生 / uni-app / Taro 怎么选★

选型题考的是判断依据,不是排名
没有普适最优解,只有匹配度。判断顺序:是否要 App → 团队技术栈 → 是否需要深度用小程序能力 → 包体积压力。把这个顺序讲出来,比背一张对比表有说服力。

原生:能力最全,成本最高

  • 所有新 API 第一时间可用(同层渲染、Skyline)
  • 性能可控,包体最省
  • 一套代码只能跑微信(多端要重写)
  • 适合:只做微信、对性能敏感、要用最新能力

跨端框架:一套代码多端

  • uni-app:Vue 技术栈,编译期,App 端成熟
  • Taro:React 技术栈,运行时,生态好
  • 新 API 支持有滞后,复杂能力要写原生插件
  • 适合:多端投放、团队已有 Web 技术栈
面试怎么答选型看四件事:要不要 App、团队是 React 还是 Vue、是否需要马上用小程序新能力、主包体积是否紧张。要 App 且团队用 Vue 就 uni-app,团队是 React 就 Taro。只做微信又要用 Skyline 这类新能力,就用原生,别为了跨端加一层复杂度。
13

rpx、样式隔离与同层渲染

三个几乎每场都问的细节
rpx 是按 750 设计稿等比缩放的单位;样式隔离决定组件边界能不能穿透;同层渲染解决原生组件盖住一切的问题。三个都不是"背结论",而是要知道什么时候会踩。
rpx 的换算750rpx = 屏幕宽度(任何机型) iPhone 6(375px 宽):1rpx = 0.5px iPhone 14 Pro Max(430px 宽):1rpx ≈ 0.573px 所以:设计稿 750 宽,量多少写多少 rpx,这是它唯一正确的用法。 设计稿 375 宽时不要直接写 rpx,会整体缩小一半。
面试怎么答rpx 以 750 为基准等比缩放,所以 750 宽的设计稿可以直接按量到的数值写。原生组件曾经层级最高,z-index 无效,现在多数支持同层渲染,但 input 和 textarea 仍容易出问题,弹窗盖不住输入框就是典型场景,要单独处理。
14

速答清单

十句话,覆盖这一份的主干
01小程序为什么是双线程?
逻辑层跑 JS 没有 DOM,渲染层是 WebView 负责绘制,两边通过 Native 转发通信。隔离带来安全(不能直接操作 DOM)和稳定(逻辑层崩了不影响渲染),代价是 setData 的跨线程成本。
02setData 慢在哪?
序列化 + 跨线程传输 + 反序列化 + WXML 树 diff,成本跟数据量正相关。优化就是只传变化项、合并调用、压频率,长列表上虚拟列表。
03wx:key 不写会怎样?
列表 diff 只能按下标复用节点,中间插入或删除会导致后续节点数据整体重写,表现为状态错乱或性能骤降。
04页面栈最多几层?
10 层。navigateTo 压栈,超过就失败;switchTab 会清空栈只留 tab 页;reLaunch 关掉所有页面。
05主包和分包的限制?
主包 2MB,单个分包 2MB,总包 20MB。分包之间不能互相引用,公共依赖放主包。用 preloadRule 预下载减少跳转白屏。
06登录为什么不能前端换 openid?
因为要用 appid + secret,secret 放在客户端等于公开。正确流程是 code 给后端,后端换 openid 和 session_key,只把自定义 token 返回前端。
07session_key 为什么不能下发?
它能解密手机号等加密数据,一旦到前端,任何拿到它的人都能解密。必须留在服务端会话里。
08uni-app 和 Taro 的本质区别?
uni-app 是编译期,产物就是各端原生语法,性能接近原生;Taro 3 是运行时,在小程序里跑真 React,能力全但多一层映射和包体开销。
09为什么从详情页返回列表页,数据不刷新?
因为返回只触发 onShow,不走 onLoad 和 mounted。刷新逻辑要放 onShow。
10原生组件盖住弹窗怎么解决?
多数组件已支持同层渲染,用真机和高版本基础库确认;input / textarea 仍顽固,用 cover-view 或改用 view 模拟,必要时先 hideKeyboard 再显示弹窗。