四、模板语法与数据绑定:Mustache语法、v-bind与v-model、计算属性与侦听器、列表渲染与条件渲染
这一章,我们来聊聊小程序里最核心的“面子工程”——模板语法与数据绑定。
说白了,就是你怎么把 JS 里的数据,优雅地展示到页面上,以及用户操作怎么反馈回数据层。我刚开始做小程序时,总觉得这玩意儿跟 Vue 差不多,结果一上手就踩了几个坑。嗯,咱们一个一个说。
4.1 Mustache 语法:花括号里的秘密
Mustache 语法,就是那对双花括号 {{ }}。它干的事很简单:把数据“插”到模板里。
核心用法:
<view>{{ message }}</view>
<view>{{ user.name }}</view>
<view>{{ price + 10 }}</view>
<view>{{ isVIP ? '尊贵会员' : '普通用户' }}</view>
我个人习惯把 Mustache 当成“只读通道”。它只能展示数据,不能修改数据。你可以在里面写简单的表达式——加减乘除、三元运算、字符串拼接都行。
避坑指南:我曾经在 Mustache 里写了个 new Date(),结果小程序直接报错。记住,花括号里不能调用函数,不能写复杂的逻辑语句。所有复杂计算,请交给后面的计算属性或方法。
还有一点要注意:Mustache 不能用在属性值里。比如 <view id="{{ itemId }}"> 这种写法,在微信小程序里是不行的。那怎么办?用 v-bind 啊。
4.2 v-bind:动态绑定属性
v-bind 的作用,就是把数据绑定到 HTML 属性上。比如图片的 src、链接的 href、元素的 class 或 style。
<!-- 正确写法 -->
<image :src="avatarUrl"></image>
<view :class="{ active: isActive }">动态样式</view>
<view :style="{ color: textColor, fontSize: size + 'px' }">动态内联样式</view>
这里我用了 Vue 的简写 :,其实就是 v-bind: 的语法糖。我个人更推荐用简写,代码看起来干净。
我在项目中遇到过一个问题:动态绑定图片路径时,如果路径是变量拼接的,一定要确保路径正确。比如:
// 错误示范
data: {
imgUrl: '/images/' + itemId + '.png'
}
// 正确做法:用模板字符串
data: {
imgUrl: `/images/${itemId}.png`
}
为什么?因为小程序里路径解析跟浏览器不一样,拼接字符串容易出幺蛾子。用模板字符串,清晰又安全。
4.3 v-model:双向绑定的甜蜜与陷阱
v-model 是双向绑定的核心。它把输入框的值和 data 里的数据绑定在一起,用户改了输入框,数据自动更新;数据变了,输入框也自动刷新。
<input v-model="username" placeholder="请输入用户名">
<textarea v-model="description"></textarea>
<checkbox-group v-model="hobbies">
<checkbox value="reading">阅读</checkbox>
<checkbox value="coding">编程</checkbox>
</checkbox-group>
嗯,这里要注意:v-model 本质上是个语法糖。它等于 :value + @input。所以如果你需要自定义输入事件,可以直接拆开写。
我曾经踩过的坑:在微信小程序里,v-model 对原生组件的支持并不完美。比如 picker、switch 这些组件,用 v-model 有时会失效。我的建议是:复杂表单组件,手动绑定 :value 和 @change 事件,更可控。
另外,v-model 的修饰符也很有用:
| 修饰符 | 作用 | 示例 |
|---|---|---|
.lazy |
在 change 事件后同步,而非 input 时 | v-model.lazy="text" |
.number |
自动将输入转为数字类型 | v-model.number="age" |
.trim |
自动去除首尾空格 | v-model.trim="name" |
你想想看,如果用户输入年龄时带了个空格,或者输入了字符串,用 .number 就能自动帮你转成数字,省去手动 parseInt 的麻烦。
4.4 计算属性与侦听器:数据的“智能管家”
计算属性(computed)和侦听器(watch),是处理派生数据和响应变化的利器。
4.4.1 计算属性:有缓存的计算
计算属性,说白了就是“根据已有数据,自动计算新数据”。它跟方法(methods)最大的区别是:计算属性有缓存。只要依赖的数据没变,多次访问计算属性会直接返回缓存结果,不会重复计算。
computed: {
// 全名:自动拼接
fullName() {
return this.firstName + ' ' + this.lastName
},
// 购物车总价:自动求和
cartTotal() {
return this.cartItems.reduce((sum, item) => sum + item.price * item.count, 0)
},
// 过滤后的列表:自动筛选
activeUsers() {
return this.users.filter(user => user.isActive)
}
}
我个人习惯把计算属性当成“视图层的预处理”。比如后端返回的时间戳,我就在计算属性里转成“2024-01-15 14:30”这种格式。这样模板里直接 {{ formattedTime }} 就行,干净利落。
避坑指南:计算属性默认是只读的。如果你试图给计算属性赋值,比如 this.fullName = '张三',会报错。如果确实需要修改,可以写 setter:
computed: {
fullName: {
get() {
return this.firstName + ' ' + this.lastName
},
set(newVal) {
const names = newVal.split(' ')
this.firstName = names[0]
this.lastName = names[1] || ''
}
}
}
不过说实话,我在项目中很少用 setter,因为容易造成数据流混乱。能用 getter 解决的,就别搞 setter。
4.4.2 侦听器:监听变化,执行副作用
侦听器(watch)用来监听某个数据的变化,然后执行一些操作。比如搜索框输入时,自动发起请求;或者用户切换选项时,重置相关数据。
watch: {
// 监听搜索关键词
keyword(newVal, oldVal) {
// 防抖处理
clearTimeout(this.timer)
this.timer = setTimeout(() => {
this.search(newVal)
}, 300)
},
// 监听用户登录状态
isLogin(newVal) {
if (newVal) {
this.fetchUserInfo()
} else {
this.clearUserData()
}
},
// 深度监听对象
'user.profile': {
handler(newVal) {
console.log('用户资料变了', newVal)
},
deep: true,
immediate: true // 立即执行一次
}
}
我在项目中遇到过一个问题:监听一个对象时,如果不加 deep: true,对象内部属性变了,侦听器不会触发。但加了 deep 又要注意性能,因为深度监听会递归遍历所有属性。我的建议是:尽量监听具体属性路径,比如 'user.profile.name',而不是整个对象。
为什么会这样?因为 JavaScript 的对象是引用类型,直接监听对象只能监听到引用变化,内部属性变化需要深度监听。
4.5 列表渲染与条件渲染:循环与判断的艺术
列表渲染(v-for)和条件渲染(v-if / v-show),是模板里最常用的控制结构。
4.5.1 v-for:循环渲染列表
<!-- 遍历数组 -->
<view v-for="(item, index) in list" :key="item.id">
{{ index }} - {{ item.name }}
</view>
<!-- 遍历对象 -->
<view v-for="(value, key, index) in obj" :key="key">
{{ key }}: {{ value }}
</view>
<!-- 遍历数字范围 -->
<view v-for="n in 10" :key="n">
第 {{ n }} 个
</view>
嗯,这里有个关键点:一定要加 :key。key 的作用是帮助框架识别哪些元素被修改、添加或删除。如果不加 key,列表更新时可能会出问题,比如输入框内容错位。
我曾经踩过的坑:用 v-for 渲染列表时,如果列表项有输入框,而且用户输入了内容,然后列表重新排序了——你会发现输入框里的内容跟着元素跑了,而不是跟着数据跑。这就是没加 key 的后果。加了 key 后,框架会按 key 来复用元素,内容就不会乱跑。
另外,v-for 和 v-if 不要放在同一个元素上。因为 v-for 的优先级比 v-if 高,这样会导致每次循环都判断一次,性能浪费。正确做法是:用 <template> 包裹,或者先过滤再渲染。
<!-- 错误:v-for 和 v-if 同层 -->
<view v-for="item in list" v-if="item.isActive" :key="item.id">
{{ item.name }}
</view>
<!-- 正确:用 template 包裹 -->
<template v-for="item in list" :key="item.id">
<view v-if="item.isActive">
{{ item.name }}
</view>
</template>
<!-- 更推荐:先计算过滤 -->
<view v-for="item in activeList" :key="item.id">
{{ item.name }}
</view>
4.5.2 v-if / v-show:条件渲染的选择
v-if 和 v-show 都能控制元素显示隐藏,但机制完全不同:
| 特性 | v-if | v-show |
|---|---|---|
| 渲染方式 | 条件为 false 时,元素不渲染(DOM 中不存在) | 条件为 false 时,元素渲染但 display: none |
| 切换开销 | 大(销毁/重建 DOM) | 小(仅切换 CSS) |
| 初始渲染开销 | 小(只渲染条件为 true 的) | 大(所有元素都渲染) |
| 适用场景 | 条件很少改变,或需要懒加载 | 条件频繁切换,如 Tab 切换 |
我个人习惯:如果条件在运行时几乎不变,用 v-if。比如用户权限判断,管理员看到管理按钮,普通用户看不到——这种场景用 v-if,因为普通用户根本不需要渲染那个按钮。
如果条件频繁切换,用 v-show。比如弹窗的显示隐藏、Tab 内容切换。用 v-show 避免了频繁的 DOM 创建销毁,性能更好。
总结一下本章要点:
- Mustache 语法:只读展示,不能写复杂逻辑
- v-bind:动态绑定属性,用
:简写 - v-model:双向绑定,复杂表单建议手动拆开
- 计算属性:有缓存,适合派生数据
- 侦听器:监听变化,执行副作用,注意 deep 性能
- v-for:必须加 key,不要和 v-if 同层
- v-if vs v-show:根据切换频率选择
好了,这一章的内容就到这里。模板语法和数据绑定,说白了就是“数据怎么来,怎么展示,怎么回去”这三件事。把这些搞明白了,小程序的页面交互你就掌握了七八成。下一章我们聊聊事件处理,那才是真正让页面“活”起来的关键。