4、框架基础:数据绑定、条件渲染(a:if)、列表渲染(a:for)—— 让页面“活”起来

各位同学,欢迎来到第四章。

前面几章我们搭好了架子,写了静态页面。但说实话,静态页面就像一张照片,好看是好看,但不会动。今天我们要聊的,就是让页面真正“活”起来的技术——数据绑定、条件渲染和列表渲染。

我个人习惯把这三样东西称为“框架三件套”。你想想看,一个动态页面无非就是:数据变了界面跟着变(数据绑定)、根据条件显示不同内容(条件渲染)、把一堆数据循环展示出来(列表渲染)。掌握了这三样,你就能写出真正可交互的小程序了。

4.1 数据绑定:让数据和界面“同步”

数据绑定,说白了就是“数据变了,页面自动变”。你不用手动去操作DOM,不用像以前写jQuery那样$('#xxx').text('新内容')。在支付宝小程序里,你只需要改数据,框架会自动帮你更新视图。

4.1.1 基础数据绑定

先看个最简单的例子。在页面的JS文件中,我们定义数据:

// pages/index/index.js
Page({
  data: {
    message: '你好,支付宝小程序!',
    price: 99.8,
    isVip: true
  }
})

然后在AXML模板中,用双花括号 {{ }} 把数据包起来:

<view>{{ message }}</view>
<view>价格:{{ price }} 元</view>
<view>VIP用户:{{ isVip ? '是' : '否' }}</view>

这里要注意,{{ }} 里面可以写简单的表达式,比如三元运算符、算术运算、字符串拼接等。但别写太复杂的逻辑,比如循环、函数调用——这些应该在JS里处理好。

我的小技巧: 我习惯在 data 里把所有字段都初始化好,哪怕初始值是空字符串或 null。这样后面排查问题的时候,一眼就能看出数据结构,不会出现“undefined”报错。

4.1.2 数据更新:this.setData()

数据绑定的核心是更新数据。在支付宝小程序里,更新数据必须用 this.setData() 方法。直接修改 this.data 是不会触发视图更新的——这是我刚入坑时踩过的坑。

// 正确做法
this.setData({
  message: '数据已更新!',
  price: 88.8
})

// 错误做法(不会触发视图更新)
this.data.message = '这样不行'

为什么会这样?因为 setData 会通知框架:“数据变了,快重新渲染!” 而直接赋值只是改了内存里的值,框架并不知道。

注意: setData 是异步的。如果你需要在数据更新后立即执行某些操作,请在回调里写:
this.setData({ key: value }, () => { // 更新完成后的操作 })

4.1.3 对象和数组的更新

实际项目中,我们经常要更新对象里的某个字段,或者数组里的某个元素。这时候有个小技巧:

// 更新对象
this.setData({
  'user.name': '张三',
  'user.age': 25
})

// 更新数组元素
this.setData({
  'list[0].title': '第一项的新标题'
})

用路径字符串的方式,可以精准更新深层数据,不用把整个对象都传进去。我在项目中处理复杂表单时,这个技巧帮我省了不少代码量。

4.2 条件渲染:a:if 和 a:elif 和 a:else

条件渲染,就是根据条件决定某个元素显示还是隐藏。支付宝小程序提供了 a:ifa:elifa:else 三个指令。

4.2.1 基本用法

<view a:if="{{ score >= 90 }}">优秀</view>
<view a:elif="{{ score >= 60 }}">及格</view>
<view a:else>不及格</view>

这段代码会根据 score 的值,只显示其中一个 view。注意 a:elifa:else 必须紧跟在 a:if 后面,中间不能有其他元素。

4.2.2 a:if 与 hidden 的区别

很多新手会问:用 a:if 和用 hidden 属性有什么区别?

对比项 a:if hidden
渲染方式 条件为 false 时,元素不渲染(DOM中不存在) 条件为 false 时,元素隐藏(display: none)
切换开销 大(需要创建/销毁DOM) 小(只切换CSS)
适用场景 条件不频繁切换,或初始条件为 false 条件频繁切换,或初始条件为 true

简单来说:如果某个模块很少切换显示状态,用 a:if 更省资源;如果频繁切换(比如点击按钮显示/隐藏),用 hidden 性能更好。

我曾经踩过的坑: 有个列表页,每个商品项都有一个“展开详情”的功能。我一开始用了 a:if,结果每次展开都要重新创建DOM,列表一多就卡顿。后来改成 hidden,流畅多了。记住:频繁切换用 hidden,不频繁切换用 a:if。

4.3 列表渲染:a:for

列表渲染,就是把一个数组循环渲染到页面上。这是做列表页、商品展示、评论列表等场景的必备技能。

4.3.1 基本用法

<view a:for="{{ items }}" a:key="id">
  {{ index }} - {{ item.name }}
</view>

这里有几个关键点:

  • a:for 绑定一个数组
  • 默认情况下,当前项用 item 表示,索引用 index 表示
  • a:key 用来提高渲染性能,建议每次都加上

4.3.2 自定义变量名

如果 itemindex 与其他变量冲突,可以用 a:for-itema:for-index 重命名:

<view a:for="{{ users }}" a:for-item="user" a:for-index="idx" a:key="user.id">
  {{ idx + 1 }}. {{ user.name }} - {{ user.age }}岁
</view>

我个人习惯在嵌套循环时一定要重命名,否则内层循环的 item 会覆盖外层,很容易出 bug。

4.3.3 a:key 的重要性

很多初学者会忽略 a:key,觉得不加也能跑。确实能跑,但性能会差很多。

为什么?因为框架需要知道哪些列表项是新增的、哪些是删除的、哪些是移动的。如果没有 a:key,框架只能按索引来对比,一旦列表顺序变了,它可能会把整个列表重新渲染。加上 a:key 后,框架就能精准地只更新变化的部分。

我的建议: a:key 的值要选唯一且稳定的字段,比如数据库里的 id。不要用 index,因为 index 会随着列表变化而改变,起不到标识作用。

4.3.4 列表嵌套与复杂场景

实际项目中,列表嵌套很常见。比如一个分类列表,每个分类下又有商品列表:

<view a:for="{{ categories }}" a:for-item="category" a:key="category.id">
  <view>{{ category.name }}</view>
  <view a:for="{{ category.products }}" a:for-item="product" a:key="product.id">
    {{ product.name }} - ¥{{ product.price }}
  </view>
</view>

这里要注意,内层循环的 itemindex 会覆盖外层,所以必须用 a:for-itema:for-index 重命名。否则内层拿到的 item 是商品对象,外层拿到的 item 也是商品对象——这就乱套了。

4.4 综合实战:一个简单的商品列表

好了,理论讲完了。我们来写一个综合例子,把今天学的三个知识点串起来。

假设我们要做一个商品列表页,有以下几个功能:

  • 展示商品列表(列表渲染)
  • 根据库存显示“有货”或“缺货”(条件渲染)
  • 点击商品切换选中状态(数据绑定)

先看数据:

Page({
  data: {
    products: [
      { id: 1, name: '蓝牙耳机', price: 199, stock: 10, selected: false },
      { id: 2, name: '充电宝', price: 89, stock: 0, selected: false },
      { id: 3, name: '数据线', price: 29, stock: 50, selected: false }
    ]
  },
  toggleSelect(e) {
    const id = e.currentTarget.dataset.id
    const products = this.data.products.map(item => {
      if (item.id === id) {
        return { ...item, selected: !item.selected }
      }
      return item
    })
    this.setData({ products })
  }
})

再看模板:

<view a:for="{{ products }}" a:for-item="product" a:key="product.id" 
      class="product-item {{ product.selected ? 'selected' : '' }}"
      data-id="{{ product.id }}" bindtap="toggleSelect">
  <view>{{ product.name }}</view>
  <view>¥{{ product.price }}</view>
  <view a:if="{{ product.stock > 0 }}" class="in-stock">有货</view>
  <view a:else class="out-of-stock">缺货</view>
</view>

这个例子虽然简单,但包含了今天讲的三个核心知识点。你想想看,如果没有框架,你要手动操作DOM来更新每个商品的选中状态、库存显示——那得写多少代码?

4.5 避坑指南

最后,分享几个我实际项目中遇到的坑:

  1. setData 的数据量不要太大。 单次 setData 的数据量建议控制在 1MB 以内,否则可能引起页面卡顿。如果数据量很大,考虑分页加载。
  2. a:key 的值不要用 index。 前面说过了,index 不稳定。用唯一 id 最靠谱。
  3. 条件渲染不要和 hidden 混用。 同一个元素,要么用 a:if,要么用 hidden,别两个都用,逻辑会乱。
  4. 列表渲染时,注意 this 指向。 在事件处理函数中,this 指向 Page 实例,但在回调函数或箭头函数中,this 可能会丢失。我习惯用箭头函数或者提前保存 const that = this
我曾经踩过的坑: 有一次做购物车页面,商品列表里每个商品都有数量选择器。我用了 a:for 渲染,但没加 a:key。结果用户修改数量后,整个列表重新渲染,滚动位置也丢了,体验极差。加上 a:key 后问题解决。所以,a:key 不是可选项,是必选项。

好了,这一章的内容就到这里。数据绑定、条件渲染、列表渲染,这三个基础操作你掌握了,就能写出大部分动态页面了。下一章我们会聊事件处理——怎么响应用户的点击、输入等操作。到时候你会发现,这三样东西和事件处理结合起来,才是真正的“活”页面。

去写代码吧,动手试试。光看是学不会的。