前言
最近用 uni-app 写了一个项目,本地用 H5 模式开发和调试时一切正常,页面、交互、数据更新都没有问题。 但当代码部署到 微信小程序 后,却陆续出现了一些诡异的问题: 有的逻辑在 H5 能跑,小程序却完全不生效;有的组件在 H5 表现正常,小程序却怎么调都不对。
一开始我以为是自己代码写得有问题,直到踩坑越来越多才发现—— H5 和微信小程序的差异,远比我最初想象的要大得多。
所以想写篇博客记录一下开发中遇到的坑以及是如何解决的
同一套 uni-app / Vue 代码,H5 跑得好好的,到了微信小程序却各种翻车。
v-show 不生效、table 里 ref 调不到、输入框值怎么都回不去……
这些问题并不是偶发 bug,而是微信小程序底层架构与浏览器完全不同导致的必然结果。
本文从实际场景出发,把我在实际开发中高频踩到的坑系统整理一遍,树立一下小程序思维。
一、核心结论先行
一句话总结微信小程序的本质:
微信小程序 = 双线程 + setData + 自定义组件
而 H5 是:
浏览器 + DOM + 同步更新
你在开发中遇到的 80% 问题,都可以回到这条差异上来解释。
二、什么是「双线程模型」?
1. H5(浏览器)的运行方式
在浏览器中:
- JS 可以直接操作 DOM
- 数据变化 → 视图几乎同步更新
v-model、input.value都是“最终裁判”
可以理解为:
JS 线程
↓
DOM
↓
页面立刻变化
这是一个单一运行环境。
2. 微信小程序的运行方式
微信小程序为了安全性和可控性,彻底禁用了 DOM,改成了双线程架构:
┌──────────────┐
│ 逻辑层 JS │ ← 你的 Vue / JS 代码
└───────▲──────┘
│ setData(JSON)
┌───────▼──────┐
│ 视图层 WXML │ ← 渲染、组件内部状态
└──────────────┘
特点:
- 逻辑层和视图层完全隔离
- 只能通过
setData传 JSON 数据 - 所有 UI 更新都是异步的
JS 不能直接改 UI,只能“请求”视图层更新。
三、为什么「H5 能跑,小程序翻车」?
下面这些你踩过的坑,其实都源于同一个原因。
四、v-show 在 th / td 不生效
1. H5 中的认知
<td v-show="show">库存</td>
在浏览器里:
- 本质是
display: none - DOM 仍然存在
- table 布局可以正确处理
2. 小程序中为什么不行?
原因有两个:
-
小程序没有真实的 table DOM
table / tr / td只是被编译成普通 view- 表格布局是 uni-ui 模拟的
-
表格结构在视图层一次性计算
v-show只改逻辑层状态- 列宽、行结构不会重新计算
结果就是:
数据变了,但表格结构不重算
✅ 正确做法
<th v-if="showCol">库存</th>
<td v-if="showCol">{{ row.stock }}</td>
在 table 结构里:只用 v-if,不要用 v-show。
五、table 点击事件里调用 ref 不生效
1. 常见写法
openPopup() {
this.$refs.popupRef.open()
}
H5:大多能跑
小程序:经常无效
2. 真正原因
在小程序中,事件链路是:
点击 td(视图层)
↓
事件传到逻辑层
↓
你同步调用 ref
↓
popup 组件:
- 可能还没渲染
- 可能被 v-show 隐藏
ref 在小程序中是异步可用的。
✅ 正确用法
openPopup() {
this.$nextTick(() => {
this.$refs.popupRef?.open()
})
}
并且:
- popup 尽量用
v-if - 不要指望 ref 在同一个 tick 里可用
其实 $refs + $nextTick 在微信小程序依然不生效的真实原因
很多人在 H5 中已经形成了一个条件反射式写法:
openPopup() {
this.$nextTick(() => {
this.$refs.popupRef?.open()
})
}
在 H5 / WebView 下,这种写法基本 100% 生效; 但在 微信小程序(包括 uni-app)中,这段代码依然可能完全不生效。
❌ 为什么有时候 $nextTick + ref 在小程序里也不可靠?
核心原因只有一句话:
$nextTick只能保证 Vue 虚拟 DOM 更新完成,不能保证小程序组件实例已经 ready。
在微信小程序中:
- 组件是 自定义组件(不是 DOM)
- 渲染发生在 渲染层线程
- 父组件逻辑运行在 逻辑层线程
- 组件实例的创建、挂载、通信,都需要经过
setData的异步桥接
因此会出现以下真实现象:
| 你以为 | 实际发生 |
|---|---|
$nextTick 后 ref 一定存在 | ref 变量存在,但组件实例尚未 ready |
this.$refs.popupRef.open() | 调用时组件方法尚未注入 |
| 控制台不报错 | UI 没任何反应 |
✅ 小程序中最稳定、推荐的做法:状态驱动 + emit 回传
不要用父组件主动“命令式”调用子组件方法,而是:
👉 用状态控制子组件,用事件通知父组件
父组件
<Popup
:visible="popupVisible"
@close="popupVisible = false"
/>
data() {
return { popupVisible: false }
},
methods: {
openPopup() {
this.popupVisible = true
}
}
子组件
props: { visible: Boolean },
watch: {
visible(val) {
val ? this.open() : this.close()
}
},
methods: {
close() {
this.$emit('close')
}
}
🧠 一句话总结
- H5:可以命令式(ref / 调方法)
- 小程序:必须声明式(状态驱动) v-show 在 th / td 不生效
六、输入框值更新失败(uni-easyinput)
1. 现象
- H5:校验后值能回退
- 小程序:输入框仍显示非法值
2. 根因
在小程序中:
组件内部 value(视图层)
≠
v-model 绑定值(逻辑层)
如果组件不是“完全受控”:
- 视图层内部状态优先
- 你 setData 只是建议
✅ 正确组合
<uni-easyinput
v-model="row.qty"
:controlled="true"
@change="checkQty"
/>
关键点:
- 使用
change而不是input - 开启
controlled
七、input / change / blur 的正确选择
| 事件 | 建议 | 原因 |
|---|---|---|
| input | ❌ 少用 | 频繁、不可控 |
| change | ✅ 推荐 | 停止输入后的业务节点 |
| blur | ⚠️ UI 用 | 只是焦点变化 |
业务校验一律优先用 change。
八、ref、v-for、响应式的隐藏雷区
1. ref + v-for
- H5:ref 数组
- 小程序:可能只有最后一个
👉 不要依赖 ref 操作列表项
2. 深层对象更新
list[index].count = 1
在小程序中不稳定,推荐:
this.list = this.list.map(item =>
item.id === row.id ? { ...item, count: 1 } : item
)
//或者
const index=this.list.findIndex(item.id===row.id)
this.$set(this.list[index], 'count', 1)
不可变更新在小程序更安全。
九、统一解释模型(记住这个)
你遇到的所有问题,其实都是:
逻辑层变了,但视图层没有按你预期同步
对应关系:
| 现象 | 根因 |
|---|---|
| v-show 不生效 | 视图结构不重算 |
| ref 调不到 | 组件未 ready |
| input 回写失败 | 内部状态优先 |
十、小程序开发「安全原则」
- 少用
input,多用change - 表格、弹窗优先
v-if - ref 一律
nextTick - 组件要么全受控,要么不受控
- 减少 setData 次数
十一、一句话总结
H5 是“我命令页面怎么变” 小程序是“我请求页面帮我变”
当你真正接受这一点,80% 的坑都会自动消失。