微信小程序自定义组件通信:properties、observers 与 triggerEvent
微信小程序自定义组件通信可以先拆成两个方向:父页面用 properties 把数据传给子组件,子组件用 triggerEvent 把用户操作通知父页面;当多个输入字段需要共同生成组件内部状态时,再交给 observers 处理。
这篇文章用一个可复现的用户卡片组件,把声明、引用、属性更新、派生数据和自定义事件串成完整流程。示例使用原生小程序 Component,不依赖第三方框架。
微信小程序自定义组件通信的三个职责边界
| 通信需求 | 使用机制 | 数据方向 | 适合处理 |
|---|---|---|---|
| 父级传入配置或业务数据 | properties | 父级 → 子组件 | 用户对象、禁用状态、显示模式 |
| 多个字段变化后计算内部状态 | observers | 组件内部 | 派生文案、组合校验、状态同步 |
| 子组件报告用户操作 | triggerEvent | 子组件 → 父级 | 确认、选择、关闭、提交 |
不要让子组件直接承担父页面的业务请求,也不要把父级传入的对象当作组件私有状态随意改写。输入、内部派生状态和向外输出的事件分开后,组件更容易复用,也更容易定位更新链路。
第一步:声明并引用自定义组件
自定义组件和页面一样由 json、wxml、wxss、js 四个文件组成。组件目录、页面目录和配置文件的关系不清楚时,可以先看微信小程序项目目录结构详解。
{
"component": true
}
在使用它的页面 JSON 中通过 usingComponents 声明路径。下面假设组件位于项目根目录的 components/user-card/:
{
"usingComponents": {
"user-card": "/components/user-card/index"
}
}
页面 WXML 里,标签属性就是传给组件的输入,自定义事件则由页面绑定处理函数:
<user-card
user="{{currentUser}}"
disabled="{{saving}}"
bind:confirm="handleCardConfirm"
/>
properties:把外部输入定义成明确契约
properties 用来声明组件接受哪些外部属性。为属性写明类型和默认值,能让组件在页面还没拿到异步数据时仍保持可预测状态。
Component({
properties: {
user: {
type: Object,
value: null
},
disabled: {
type: Boolean,
value: false
}
},
data: {
displayName: '未选择用户'
}
})
父页面更新 currentUser 或 saving 后,新值会沿属性绑定进入组件。父级若频繁更新大对象,仍应控制 setData 的路径和数据量;可以结合微信小程序 setData 局部路径更新与性能优化检查更新范围。
observers:只保存真正需要的派生状态
当一个内部字段依赖一个或多个属性时,可以用 observers 集中同步。下面同时监听 user.name 和 disabled,生成组件展示文案:
Component({
properties: {
user: Object,
disabled: Boolean
},
data: {
displayName: '未选择用户'
},
observers: {
'user.name, disabled'(name, disabled) {
const displayName = name || '未选择用户'
this.setData({
displayName: disabled ? `${displayName}(处理中)` : displayName
})
}
}
})
数据监听器关注的是 setData 涉及的字段,而不是只在新旧值不同时触发。即使值没有变化,只要本次更新包含被监听字段,监听器仍可能执行。也不要在监听器里反复 setData 它正在监听的同一字段,否则容易形成循环。
如果文案可以直接在 WXML 中简单拼接,就不必额外存一份派生数据。observers 更适合多个输入共同驱动、需要统一校验或格式化的场景。
triggerEvent:让子组件只报告发生了什么
子组件不需要知道父页面接下来要跳转、请求接口还是写缓存。它只在交互发生时调用 triggerEvent,把必要信息放进 detail:
Component({
properties: {
user: Object,
disabled: Boolean
},
methods: {
onConfirm() {
if (this.data.disabled || !this.data.user) return
this.triggerEvent('confirm', {
userId: this.data.user.id
})
}
}
})
<view class="user-card">
<text>{{displayName}}</text>
<button disabled="{{disabled}}" bindtap="onConfirm">
确认选择
</button>
</view>
父页面从事件对象的 detail 读取组件输出,再执行业务逻辑:
Page({
data: {
currentUser: null,
saving: false
},
async handleCardConfirm(event) {
const { userId } = event.detail
if (!userId || this.data.saving) return
this.setData({ saving: true })
try {
await this.saveSelection(userId)
} finally {
this.setData({ saving: false })
}
}
})
组件内部按钮的点击仍遵循小程序事件规则。需要区分 target、currentTarget 或阻止冒泡时,可参考微信小程序 bindtap 与 catchtap 区别。
自定义事件的三个选项
| 选项 | 默认值 | 作用 | 什么时候再开启 |
|---|---|---|---|
bubbles | false | 事件是否冒泡 | 确实需要祖先节点继续接收时 |
composed | false | 事件是否穿越组件边界 | 跨越多层组件树传播时 |
capturePhase | false | 事件是否具有捕获阶段 | 需要在目标前统一拦截时 |
普通的直接父子通信通常不需要改这三个选项。先保持默认值,让事件只到声明绑定的父级;只有组件嵌套和事件传播路径有明确需求时,再逐项开启并测试。
最容易出现的五个问题
- 页面收不到事件:确认
triggerEvent('confirm')与bind:confirm的事件名完全一致。 - detail 是空的:确认第二个参数传入对象,并从父页面的
event.detail读取。 - 属性没有更新:检查页面 WXML 的属性名与组件
properties声明是否一致。 - 监听器不停执行:检查
observers是否又更新了自己监听的字段。 - 组件状态越来越难追踪:把请求、路由和全局状态修改留给父页面,组件只接收输入并抛出事件。
用静态断言核对示例契约
本文示例另存为最小原生项目片段,并用 Node.js 22 检查了组件声明、页面引用、两个属性、监听字段、自定义事件名和 detail.userId 等 12 条静态契约,全部通过。这个检查验证的是文件结构和通信约定,不等同于微信开发者工具或真机里的运行时测试。
import assert from 'node:assert/strict'
assert.equal(componentJson.component, true)
assert.equal(pageJson.usingComponents['user-card'], '/components/user-card/index')
assert.match(pageWxml, /bind:confirm="handleCardConfirm"/)
assert.match(componentJs, /triggerEvent\('confirm'/)
assert.match(componentJs, /userId:\s*this\.data\.user\.id/)
结论
微信小程序自定义组件通信的主线很清楚:properties 定义父级输入,observers 处理必要的内部派生状态,triggerEvent 把子组件操作交还给父级。先把这三个职责分开,再考虑冒泡、跨组件边界或捕获阶段,能避免大部分状态混乱和事件失联问题。
官方资料:自定义组件、Component 构造器、数据监听器、组件间通信与事件(访问于 2026-09-11);自定义组件要求基础库 1.6.3 及以上,数据监听器从基础库 2.6.1 开始支持。




