小程序自定义组件怎样通信?properties、triggerEvent、behaviors 和 selectComponent 分别做什么?
父组件通过 properties 把数据传给子组件,子组件通过 triggerEvent 触发自定义事件把数据交回父组件,这是官方推荐的两种主要方式。behaviors 用来在组件之间共享代码,类似 mixins;selectComponent 能直接拿到子组件实例,但容易让组件过度耦合,可以用 wx://component-export 和 export 定义段限制暴露的内容。
简答
比如页面里放一个商品卡片组件。页面要告诉卡片显示哪个商品,卡片被点了“加入购物车”要告诉页面。前者用
properties:子组件声明属性,父组件在 WXML 里传值。后者用triggerEvent:子组件触发一个自定义事件并带上detail,父组件用bind:事件名监听。官方文档把这两种列为父子通信的最佳方式,因为数据流向清楚:数据往下走属性,通知往上走事件,子组件不需要知道父组件是谁。
另外两个工具解决的是别的问题。
behaviors是组件间共享代码的方式,类似 mixins,引用以后属性、数据、方法、生命周期都合并进组件。selectComponent让父组件直接拿到子组件实例this,可以调用它的方法,适合属性和事件都不够灵活的场景。要注意,
triggerEvent默认不冒泡、不穿越组件边界,需要的话要设bubbles和composed。selectComponent官方不优先推荐,容易让组件互相依赖;可以引入内置 behaviorwx://component-export,用export定义段只暴露需要的内容。
父传子用 properties
子组件先在 properties 里声明属性,type 是类型,value 是初始值。父组件在 WXML 里给这个属性赋值,驼峰名可以写成连字符形式。
// 子组件 goods-card.js
Component({
properties: {
goodsId: { type: String, value: '' },
price: Number,
},
})
<!-- 页面 WXML -->
<goods-card goods-id="{{currentId}}" price="{{currentPrice}}" />
price: Number 是简写,初始值和类型默认值相同时可以只写类型,Number 的默认值是 0。命名时有几个限制:属性名不能和 data 里的字段重名;id、class、style、slot 是 WXML 保留属性,写成组件属性也没法在 WXML 里赋值;dataXxx 这样的名字也赋不了值,因为 data-xxx 会被当成 dataset。
父组件更新传入的值时,同样要经过 setData,数据量大了一样会慢,优化方式见《setData 为什么会慢?如何优化小程序的数据更新?》。
子传父用 triggerEvent
子组件调用 this.triggerEvent(事件名, detail, 选项),父组件监听的写法和监听基础组件事件一样。
// 子组件 goods-card.js
Component({
methods: {
onAddTap() {
this.triggerEvent('add', { goodsId: this.data.goodsId })
},
},
})
<!-- 页面 WXML -->
<goods-card goods-id="{{currentId}}" bind:add="onAdd" />
// 页面 JS
Page({
onAdd(e) {
console.log(e.detail.goodsId)
},
})
第三个参数是事件选项,三个选项默认都是 false:bubbles 表示是否冒泡;composed 表示能否穿越组件边界,为 false 时事件只在引用组件的那棵 Shadow 树上触发,不进入其他组件内部;capturePhase 表示是否有捕获阶段。所以默认情况下,事件只会被直接使用这个组件的地方监听到。
还要知道,triggerEvent 是同步触发的。文档提醒,在属性 observer 或数据监听器里直接调用可能导致递归更新,这时可以用 wx.nextTick 把触发放到独立任务里。
behaviors 共享代码
多个组件都需要同样的数据字段和方法时,用 Behavior 构造器定义一份,再在组件的 behaviors 里引用。
const loadingBehavior = Behavior({
data: { loading: false },
methods: {
setLoading(loading) {
this.setData({ loading })
},
},
})
Component({
behaviors: [loadingBehavior],
})
引用后,组件就有了 loading 字段和 setLoading 方法。同名时的规则要记住:属性和方法是覆盖式的,组件自己的覆盖 behavior 的,多个 behavior 之间靠后的覆盖靠前的;同名数据字段如果都是对象会合并,否则也是覆盖;生命周期函数、数据监听器这些是组合式的,都会执行,先按 behaviors 列表顺序执行,最后执行组件自己的。从基础库 2.9.2 起,Page 构造器也能用 behaviors。
所以 behaviors 解决的是“代码复用”,组件之间并不会因此共享同一份数据,每个组件实例的 loading 各是各的。
selectComponent 直接拿实例
父组件可以用 this.selectComponent(选择器) 拿到子组件实例 this,selectAllComponents 返回所有匹配的实例数组。子组件也可以用 selectOwnerComponent 拿到父组件实例。
// 父组件
const form = this.selectComponent('#order-form')
form.validate()
这样很方便,但父组件直接依赖子组件内部的方法和数据,子组件一改,父组件就跟着坏,官方文档说这种方式容易导致组件间过度耦合,不优先推荐。默认情况下,小程序与插件之间、不同插件之间的组件,用这些接口拿到的是 null。
要控制暴露的内容,可以在子组件里引入 wx://component-export,并写一个 export 定义段,这个功能从基础库 2.2.3 开始支持。对 exparser 组件框架,必须引入这个内置 behavior 才会生效。
// 子组件 order-form.js
Component({
behaviors: ['wx://component-export'],
export() {
return { validate: () => this.checkAll() }
},
methods: {
checkAll() {
return true
},
},
})
此后父组件 selectComponent('#order-form') 拿到的不再是完整实例,而是 export 返回的对象,只能调用 validate。
常见追问
父子组件想双向同步一个值怎么办?
官方文档的父子通信里列了第三种方式:双向绑定。不想用双向绑定时,就按单向数据流写,父组件传属性,子组件改值时触发事件,父组件在事件里
setData更新。
兄弟组件或跨层级组件怎么通信?
兄弟组件可以通过共同的父组件中转:一个触发事件给父组件,父组件再通过属性传给另一个。同一棵 Shadow 树里需要配合的组件,比如自己实现的表单和按钮,文档推荐用组件间关系
relations。
事件需要在页面上被祖先节点监听到,要怎么设?
只设
bubbles: true,事件会在使用这个组件的那棵 Shadow 树上冒泡。还要穿过外层组件的边界,就再设composed: true。文档的例子里,组件嵌在另一个组件的 slot 中,只开bubbles时事件跳过外层组件内部的监听器,两个都开才会依次经过外层组件内部。