前端知库

小程序自定义组件怎样通信?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 官方不优先推荐,容易让组件互相依赖;可以引入内置 behavior wx://component-export,用 export 定义段只暴露需要的内容。

父传子用 properties

子组件先在 properties 里声明属性,type 是类型,value 是初始值。父组件在 WXML 里给这个属性赋值,驼峰名可以写成连字符形式。

JavaScript
// 子组件 goods-card.js
Component({
  properties: {
    goodsId: { type: String, value: '' },
    price: Number,
  },
})
HTML
<!-- 页面 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, 选项),父组件监听的写法和监听基础组件事件一样。

JavaScript
// 子组件 goods-card.js
Component({
  methods: {
    onAddTap() {
      this.triggerEvent('add', { goodsId: this.data.goodsId })
    },
  },
})
HTML
<!-- 页面 WXML -->
<goods-card goods-id="{{currentId}}" bind:add="onAdd" />
JavaScript
// 页面 JS
Page({
  onAdd(e) {
    console.log(e.detail.goodsId)
  },
})

第三个参数是事件选项,三个选项默认都是 false:bubbles 表示是否冒泡;composed 表示能否穿越组件边界,为 false 时事件只在引用组件的那棵 Shadow 树上触发,不进入其他组件内部;capturePhase 表示是否有捕获阶段。所以默认情况下,事件只会被直接使用这个组件的地方监听到。

还要知道,triggerEvent 是同步触发的。文档提醒,在属性 observer 或数据监听器里直接调用可能导致递归更新,这时可以用 wx.nextTick 把触发放到独立任务里。

behaviors 共享代码

多个组件都需要同样的数据字段和方法时,用 Behavior 构造器定义一份,再在组件的 behaviors 里引用。

JavaScript
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 拿到父组件实例。

JavaScript
// 父组件
const form = this.selectComponent('#order-form')
form.validate()

这样很方便,但父组件直接依赖子组件内部的方法和数据,子组件一改,父组件就跟着坏,官方文档说这种方式容易导致组件间过度耦合,不优先推荐。默认情况下,小程序与插件之间、不同插件之间的组件,用这些接口拿到的是 null。

要控制暴露的内容,可以在子组件里引入 wx://component-export,并写一个 export 定义段,这个功能从基础库 2.2.3 开始支持。对 exparser 组件框架,必须引入这个内置 behavior 才会生效。

JavaScript
// 子组件 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 时事件跳过外层组件内部的监听器,两个都开才会依次经过外层组件内部。