3、组件数据与属性:data定义、properties声明、observer监听器、数据流向

好,咱们今天聊点实在的。

组件的数据和属性,说白了就是组件的「内存」和「接口」。你想想看,一个组件要是没有自己的数据,那它就是个空壳子。要是没有属性,那它跟外界就断了联系。这两样东西,是小程序组件的根基。

3.1 data定义:组件的私有状态

每个组件都有自己的 data,就像每个人都有自己的小秘密。这个 data 是组件内部用的,外部拿不到,也改不了。

// components/my-card.js
Component({
  data: {
    title: '默认标题',
    count: 0,
    isVisible: true
  }
})

我在项目中遇到过一个问题:有同事把 data 定义成了 string 类型,结果渲染的时候直接报错。记住,data 必须是个 Object,里面放键值对。

小技巧:我个人习惯把 data 里的字段按功能分组,比如 uiState 放界面状态,businessData 放业务数据。这样维护起来清爽很多。

3.2 properties声明:组件的对外接口

properties 是组件跟外界沟通的桥梁。父组件传什么,组件就收什么。有点像函数的参数。

// components/my-card.js
Component({
  properties: {
    // 简化写法
    name: String,
    
    // 完整写法
    age: {
      type: Number,
      value: 18,
      observer: function(newVal, oldVal) {
        console.log('年龄变了', oldVal, '->', newVal)
      }
    },
    
    // 复杂类型
    userInfo: {
      type: Object,
      value: {}
    }
  }
})

这里有个坑,我踩过好几次。你看 value 这个字段,它叫「默认值」。但如果你传了 undefined 进来,它不会用默认值!

注意:properties 的默认值只在「没传」的时候生效。传了 nullundefined,默认值就废了。我曾经因为这个 bug 排查了一下午。

3.3 observer监听器:数据变化的哨兵

observer 是个好东西。它能在数据变化时自动触发,就像装了个监控摄像头。

Component({
  properties: {
    score: {
      type: Number,
      observer: function(newVal, oldVal) {
        // 新值、旧值都能拿到
        if (newVal < 60) {
          this.setData({ grade: '不及格' })
        }
      }
    }
  },
  
  observers: {
    // 监听多个字段
    'score, grade': function(score, grade) {
      console.log(`分数:${score},等级:${grade}`)
    },
    
    // 支持通配符
    'user.**': function(user) {
      console.log('user 对象变了', user)
    }
  }
})

嗯,这里要注意。微信小程序有两个监听机制:observerobservers。前者是 properties 里的回调,后者是组件级别的监听器。功能差不多,但 observers 更灵活,能监听多个字段。

核心区别:
  • observer:写在 properties 里,只监听单个属性
  • observers:写在组件根级别,支持多字段、通配符

3.4 数据流向:单向数据流

小程序组件的数据流是单向的。什么意思?

父组件 → 通过 properties 传给子组件。子组件不能直接改 properties,只能通过 triggerEvent 通知父组件去改。

// 子组件中
methods: {
  onTap() {
    // 不能直接 this.properties.count++
    // 应该这样:
    this.triggerEvent('change', { delta: 1 })
  }
}

// 父组件中
<my-counter bind:change="onCounterChange" />

Page({
  onCounterChange(e) {
    this.setData({
      count: this.data.count + e.detail.delta
    })
  }
})

这个设计其实很聪明。你想想看,如果子组件能随便改父组件的数据,那调试起来得多痛苦?数据流乱了,bug 就来了。

我的经验:刚开始做组件时,我总想「省事」,直接在子组件里改 properties。结果项目大了以后,数据流乱成一锅粥。后来老老实实走事件通知,反而省心多了。

3.5 实战:做一个计数器组件

光说不练假把式。咱们写个完整的计数器组件,把上面这些知识点串起来。

// components/counter/counter.js
Component({
  properties: {
    min: {
      type: Number,
      value: 0
    },
    max: {
      type: Number,
      value: 100
    },
    step: {
      type: Number,
      value: 1
    }
  },
  
  data: {
    current: 0,
    canDecrease: false,
    canIncrease: true
  },
  
  observers: {
    'min, max, current': function(min, max, current) {
      this.setData({
        canDecrease: current > min,
        canIncrease: current < max
      })
    }
  },
  
  methods: {
    onDecrease() {
      if (!this.data.canDecrease) return
      const newVal = this.data.current - this.data.step
      this.setData({ current: newVal })
      this.triggerEvent('change', { value: newVal })
    },
    
    onIncrease() {
      if (!this.data.canIncrease) return
      const newVal = this.data.current + this.data.step
      this.setData({ current: newVal })
      this.triggerEvent('change', { value: newVal })
    }
  }
})
<!-- components/counter/counter.wxml -->
<view class="counter">
  <button disabled="{{!canDecrease}}" bindtap="onDecrease">-</button>
  <text>{{current}}</text>
  <button disabled="{{!canIncrease}}" bindtap="onIncrease">+</button>
</view>

这个组件里,properties 定义了外部接口,data 管理内部状态,observers 自动计算按钮是否可用,triggerEvent 把变化通知给父组件。一套组合拳下来,组件既独立又协作。

总结一下:
概念 作用 谁在用
data 私有数据,组件内部使用 组件自己
properties 对外接口,接收外部数据 父组件传值
observer/observers 监听数据变化,自动响应 组件内部逻辑
triggerEvent 向父组件发送事件 子组件通知父组件

说白了,数据流就是「父传子用 properties,子传父用 events」。这个规矩守住了,组件化开发就成功了一半。

我记得刚带团队时,有个新人问我:「为什么不能直接改 properties?」我让他试了试,结果页面没反应,控制台还报了个警告。嗯,实践出真知嘛。