3、组件通信(上):父传子(properties)、子传父(triggerEvent)、兄弟组件通信

组件通信,说白了就是组件之间怎么「说话」。

我刚开始写小程序时,觉得组件化就是拆文件。后来发现,拆开容易,让它们配合起来才是真功夫。今天咱们先聊三种最基础的通信方式:父传子、子传父、兄弟组件通信。

3.1 父传子:properties

这是最直观的一种通信。父组件把数据往下扔,子组件伸手接住就行。

核心思路:父组件在标签上写属性,子组件用 properties 声明接收。

举个例子。我有一个商品卡片组件,父页面要把商品信息传给它。

子组件定义(product-card/index):

// product-card.js
Component({
  properties: {
    // 简单写法
    title: String,
    price: Number,
    
    // 完整写法(我推荐这种,可读性更好)
    product: {
      type: Object,
      value: {},
      observer: function(newVal, oldVal) {
        console.log('商品数据变了', newVal)
      }
    }
  }
})
<!-- product-card.wxml -->
<view class="card">
  <text>{{title}}</text>
  <text>¥{{price}}</text>
  <image src="{{product.image}}"></image>
</view>

父组件使用:

<!-- 父页面.wxml -->
<product-card 
  title="蓝牙耳机" 
  price="199" 
  product="{{productData}}" 
/>

我的习惯:能用对象传就别拆成单个属性。比如 product 对象一次传完,比 title、price、image 一个个传清爽多了。我在项目中遇到过传了十几个属性的情况,改起来想哭。

3.2 子传父:triggerEvent

子组件想跟父组件说话,怎么办?不能直接改父组件的数据,那样就乱套了。

小程序的做法是:子组件「喊一声」,父组件「听着」。这个喊的过程就叫 triggerEvent。

子组件触发事件:

// product-card.js
Component({
  methods: {
    onAddToCart() {
      // 第一个参数:事件名
      // 第二个参数:要传递的数据
      // 第三个参数:配置(一般用默认)
      this.triggerEvent('addcart', {
        id: this.properties.product.id,
        name: this.properties.title
      })
    }
  }
})
<!-- product-card.wxml -->
<button bindtap="onAddToCart">加入购物车</button>

父组件监听事件:

<!-- 父页面.wxml -->
<product-card 
  title="蓝牙耳机" 
  bind:addcart="onAddCartHandler" 
/>
// 父页面.js
Page({
  onAddCartHandler(event) {
    const { id, name } = event.detail
    console.log('收到子组件消息:', id, name)
    // 这里可以更新购物车数据
  }
})

我曾经踩过的坑:事件名不要用驼峰,比如 addCart。小程序里事件名统一用小写,否则 bind:addCart 可能监听不到。改成 bind:addcart 就稳了。

3.3 兄弟组件通信

兄弟组件之间怎么通信?说白了,它们没有直接关系,不能直接传。

常用的方案有三种:

方案 适用场景 复杂度
通过父组件中转 兄弟数量少,关系简单
全局事件总线 兄弟多,层级深
全局数据(app.globalData) 跨页面、跨组件共享

方案一:父组件中转(我最常用的)

兄弟 A 触发事件给父组件,父组件再通过 properties 传给兄弟 B。

<!-- 父组件.wxml -->
<!-- 兄弟A:搜索框 -->
<search-box bind:search="onSearch" />

<!-- 兄弟B:商品列表 -->
<product-list keyword="{{keyword}}" />
// 父组件.js
Component({
  data: {
    keyword: ''
  },
  methods: {
    onSearch(event) {
      // 兄弟A说:我搜了"耳机"
      // 父组件更新数据,兄弟B自动收到
      this.setData({
        keyword: event.detail.keyword
      })
    }
  }
})

方案二:全局事件总线

小程序没有内置的事件总线,但我们可以用 app 对象或者自己写一个。

// app.js
App({
  globalData: {
    eventBus: {
      listeners: {},
      on(event, callback) {
        if (!this.listeners[event]) {
          this.listeners[event] = []
        }
        this.listeners[event].push(callback)
      },
      emit(event, data) {
        this.listeners[event]?.forEach(cb => cb(data))
      },
      off(event) {
        delete this.listeners[event]
      }
    }
  }
})
// 兄弟A组件.js
const app = getApp()
Component({
  methods: {
    sendMessage() {
      app.globalData.eventBus.emit('updateList', { id: 1 })
    }
  }
})
// 兄弟B组件.js
const app = getApp()
Component({
  lifetimes: {
    attached() {
      app.globalData.eventBus.on('updateList', (data) => {
        console.log('收到兄弟消息:', data)
        this.setData({ list: data })
      })
    },
    detached() {
      // 别忘了清理,防止内存泄漏
      app.globalData.eventBus.off('updateList')
    }
  }
})

我的建议:兄弟通信优先用父组件中转。虽然代码多写几行,但数据流向清晰,调试起来不头疼。全局事件总线用多了,项目会变成「事件地狱」,你想想看,到处都在 emit 和 on,出了问题都不知道谁发的。

3.4 避坑指南

  • properties 是单向的:子组件不要直接修改 properties 的值。虽然小程序不报错,但会警告。真想改,自己 data 里存一份副本。
  • triggerEvent 的冒泡:默认不冒泡。如果你想让事件穿透组件层,第三个参数传 { bubbles: true }
  • 兄弟通信记得清理:用全局事件总线时,组件销毁了一定要 off 掉监听。我见过一个项目,页面切走了还在收消息,数据乱得没法看。

嗯,这三种通信方式搞明白了,大部分组件协作场景都能应付。下一节咱们聊更高级的通信方式,比如 selectComponent 和 relations。到时候你会发现,组件通信其实可以更灵活。