slot 模式

每个组件实例都拥有独立的 Shadow 树,代表组件自身模板中的节点结构。当组件的使用者在组件标签内放置子节点时,这些子节点会被视为 slot 内容,放置到 <slot> 的位置上。换句话说,<slot> 是一个占位符,代表 slot 内容所在的位置。

自定义组件使用 <slot> 时,slot 有三种工作模式可选。每个组件都可以任选其一。

  • 单一 slot 是默认情况,仅支持一个 <slot> 节点;
  • 多 slot 支持多个 <slot>,每个 slot 需要有一个名字 <slot name="xxx">
  • 动态 slot 支持在 wx:for 内部使用 <slot>,同时可以向 slot 内容节点树传递数据。

单一 slot

默认情况下激活的是单一 slot 模式。这种模式具有最优的性能。

在这个模式下,自定义组件 WXML 中只能包含一个 <slot> 节点。例如:

<!-- custom-component.wxml -->
<view>
  <slot />
</view>

使用这个自定义组件时,传入内容节点:

<custom-component>
  <view> content </view>
</custom-component>

<view> content </view> 最终会被放置到 <slot> 的位置上。

如果在自定义组件 WXML 内写了多个 <slot> 节点,只有第一个生效。

多 slot

在组件 options 中声明 multipleSlots: true 可激活多 slot 模式。

// Definition 形式的 custom-component.json
export default Component({
  options: {
    multipleSlots: true,
  },
})
// Chaining 形式的 custom-component.json
export default Component()
  .options({ multipleSlots: true })
  .register()

多 slot 模式允许自定义组件 WXML 中包含多个 <slot> 节点。每个 <slot> 都必须有名字 name,其值必须是一个唯一的字符串。例如:

<!-- custom-component.wxml -->
<view class="foo-wrapper">
  <slot name="foo" />
</view>
<view class="bar-wrapper">
  <slot name="bar" />
</view>

在使用这个自定义组件时,需要为每个 slot 内容节点指定它所在的 slot 名字,例如:

<custom-component>
  <!-- 这个 <view> 会放入 <slot name="foo"> 中 -->
  <view slot="foo"> foo content </view>

  <!-- 可以用 block 来避免额外生成一个 <view> -->
  <block slot="bar"> bar content </block>
</custom-component>

静态 slot 的 slot 内容生命周期

单一 slot 和多 slot 模式统一称为静态 slot 模式。

在静态 slot 下,即使自定义组件没有 <slot> 节点,slot 内容节点依然会被创建并触发 attached detached 等生命周期。例如,如果在自定义组件里面这么写:

<!-- custom-component.wxml -->
<view wx:if="{{ cond }}">
  <slot />
</view>

而在页面内这样写:

<custom-component>
  <view> content </view>
  <another-custom-component />
</custom-component>

如果 cond 为假,<slot> 节点就不存在,但是:

  • 即使 <slot> 节点不存在,<view> content </view><another-custom-component> 仍然会被创建,<another-custom-component>attached 生命周期依然会被触发;
  • 如果后续 cond 变成真,<slot> 节点会被创建,但是 <view> content </view><another-custom-component> 并不会被重新创建,<another-custom-component>attached 生命周期也不会再次触发。

如果想要 slot 内容与 <slot> 节点一起创建或销毁,可以使用动态 slot 模式。

动态 slot

动态 slot 模式允许自定义组件使用若干个 <slot> 节点。

在组件 options 中声明 dynamicSlots: true 可激活动态 slot 模式。

// Definition 形式的 custom-component.json
export default Component({
  options: {
    dynamicSlots: true,
  },
})
// Chaining 形式的 custom-component.json
export default Component()
  .options({ dynamicSlots: true })
  .register()

在使用动态 slot 时,每个 <slot> 拥有 slot 内容节点树的一份拷贝(这份拷贝会随着 <slot> 一同创建和销毁)。例如对于组件:

<!-- custom-component.wxml -->
<view class="foo">
  <slot />
</view>
<view class="bar">
  <slot />
</view>

在页面内这样写:

<custom-component>
  <view> content </view>
</custom-component>

将会最终出现两个 <view> content </view>,分别位于 <view class="foo"><view class="bar"> 内部。

同时,<slot> 节点可以传递数据字段,让 slot 内容节点树使用。这使得动态 slot 非常适合 <slot> 节点出现在 wx:for 列表内部的情况。例如对于组件:

<!-- custom-component.wxml -->
<view wx:for="{{ list }}">
  <slot list-index="{{ index }}" item="{{ item }}" />
</view>

通过 slot: 语法,在页面内可以接收到 list-indexitem

<custom-component>
  <view slot:item> {{ item }} </view>
</custom-component>

使用 slot: 接收数据时,可以用 = 来指定一个别名。此外,还可以使用 <block> 来避免生成额外的节点:

<custom-component>
  <block slot:listIndex="{{ i }}"> {{ i }} </block>
  <block slot:item> {{ item }} </block>
</custom-component>

动态 slot 模式同样支持类似多 slot 的 name 属性,不过一般并不常用。

组件间交互概述

不同的组件之间常常需要有逻辑上的交互。组件框架提供了多种组件间交互的方式。

父组件与子组件间的通信

两个有引用与被引用关系的组件,通常可以称为父组件和子组件。父组件通过 usingComponents 引用子组件,并在 WXML 中使用子组件的节点。

  • 如果父组件想要传递数据给子组件、触发子组件的特定逻辑,最佳的方式是通过 组件属性 来传递。
  • 如果子组件想要传递数据给父组件、触发父组件的特定逻辑,最佳的方式是通过 事件系统 来传递。
  • 如果想要在父组件和子组件之间同步数据,可以通过 双向绑定 来同步。

slot 也可以用于向 slot 内容节点树中传递数据,请参考 动态 slot 。

同一个 Shadow 树内节点之间的通信

在同一个 Shadow 树内,节点之间有时也需要有逻辑交互。

例如,想要自行实现 <my-form><my-button> 组件,它们是被页面这样使用的:

<!-- 页面的 WXML -->
<my-form>
  <my-button> Submit </my-button>
</my-form>

<my-button> 被点击时,需要触发在 Shadow 树上的 <my-form> 组件的提交行为。

这种情况下就需要使用 组件间关系 来实现。

直接组件实例访问

有时,上述方式都不够灵活、难以满足复杂的组件交互要求。此时可以使用 节点树访问 的方式,直接获得想要的组件实例 this

不过,这种方式如果不当使用,容易导致组件间过度耦合,因此不优先推荐。如果需要使用,建议配合 trait behaviors 来进行组件间逻辑解耦。

事件对象

事件处理函数的第一个参数是一个事件对象。

所有事件对象都具有以下属性:

属性 类型 说明 基础库版本
type String 事件类型
timeStamp Integer 事件生成时的时间戳
target Object 触发事件的组件的一些属性值集合
currentTarget Object 当前组件的一些属性值集合
mark Object 事件标记数据 2.7.1
detail Object 额外的信息(在组件触发事件时提供)

触摸和点击类事件额外具有以下属性:

属性 类型 说明
touches Array 触摸事件,当前停留在屏幕中的触摸点信息的数组
changedTouches Array 触摸事件,当前变化的触摸点信息的数组

type

代表事件的类型。

timeStamp

一个时间戳,表示页面打开到触发事件所经过的毫秒数。

target

触发事件的源组件(即冒泡事件的起点)。以点击事件为例,它表示用户最终点击到的那个组件。

属性 类型 说明
id String 事件源组件的 id
dataset Object 事件源组件的 dataset

currentTarget

事件绑定的当前组件。它指代 bindcatch 事件绑定所在的那个组件。

对于非冒泡事件,currentTargettarget 相同。

在实践中,大多数情况下使用 currentTarget 更符合预期。

属性 类型 说明
id String 当前组件的 id
dataset Object 当前组件上的 dataset

事件节点数据

在组件节点上可以附加一些自定义数据。这样,在事件中可以获取这些自定义的节点数据,用于事件的逻辑处理。

有两种方式为节点附件自定义数据:dataset 和 mark。现在更推荐使用 mark。

dataset

对于 dataset,这些自定义数据以 data- 开头,多个单词由连字符 - 连接。这种写法中,连字符写法会转换成驼峰写法,而大写字符会自动转成小写字符。如:

  • data-element-type ,最终会呈现为 event.currentTarget.dataset.elementType
  • data-elementType ,最终会呈现为 event.currentTarget.dataset.elementtype
<view data-alpha-beta="1" data-alphaBeta="2" bind:tap="bindViewTap"> DataSet Test </view>
export default Page({
  bindViewTap:function(event){
    event.currentTarget.dataset.alphaBeta === 1 // 会转为驼峰写法
    event.currentTarget.dataset.alphabeta === 2 // 大写会转为小写
  }
})

在 glass-easel 组件框架中,可以使用 data: 写法,这种写法不会进行任何转换,通常更容易理解。

<view data:alphaBeta="1" bind:tap="bindViewTap"> DataSet Test </view>
export default Page({
  bindViewTap:function(event){
    event.currentTarget.dataset.alphaBeta === 1
  }
})

mark

在基础库版本 2.7.1 以上,可以使用 mark 来识别具体触发事件的 target 节点。此外,mark 还可以用于承载一些自定义数据(类似于 dataset)。

当事件触发时,事件冒泡路径上所有的 mark 会被合并,并返回给事件回调函数(即使事件不是冒泡事件,也会有这样的 mark 合并)。这使得 mark 通常比 dataset 更实用。

<view mark:myMark="last" bind:tap="bindViewTap">
  <button mark:anotherMark="leaf" bind:tap="bindButtonTap">按钮</button>
</view>

在上述 WXML 中,如果按钮被点击,将触发 bindViewTapbindButtonTap 两个事件,事件携带的 event.mark 将包含 myMarkanotherMark 两项。

export default Page({
  bindViewTap: function(e) {
    e.mark.myMark === "last" // true
    e.mark.anotherMark === "leaf" // true
  }
})

markdataset 很相似,主要区别在于:mark 会包含从触发事件的节点到根节点上所有的 mark: 属性值;而 dataset 仅包含一个节点的 data- 属性值。

细节注意事项:

  • 如果存在同名的 mark ,父节点的 mark 会被子节点覆盖。
  • 在自定义组件中接收事件时, mark 不包含自定义组件外的节点的 mark
  • 不同于 dataset ,节点的 mark 不会做连字符和大小写转换。

touches

touches 是一个数组,每个元素为一个 Touch 对象,表示当前停留在屏幕上的触摸点。

changedTouches 与其类似,但是它包含所有变化的触摸点,包括从无变有(touchstart),位置变化(touchmove),从有变无(touchend、touchcancel)。

Touch 对象内部属性如下:

属性 类型 说明
identifier Number 触摸点的标识符
pageX, pageY Number 距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴
clientX, clientY Number 距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴

对于 canvas 组件生成的触摸类事件,Touch 对象还包含以下属性:

属性 类型 说明
x, y Number 距离 canvas 左上角的距离,横向为X轴,纵向为Y轴

detail

表示事件的详细信息,它包含的字段根据事件的不同而不同。

自定义组件在 触发事件 时可以指定这个字段的值。

点击类事件的 detail 带有的 x, y 同 pageX, pageY 代表距离文档左上角的距离。

事件绑定

常用的微信小程序事件绑定类型是 bindcatch。它们的区别是,catch 会阻止事件冒泡,可参考 事件 。

在事件绑定中使用数据绑定

在事件绑定中,可以使用数据绑定来动态指定事件处理函数。

<view bind:tap="{{ handler }}">Click me</view>

此时,this.data.handler 应当是一个字符串,表示事件处理方法函数名。如果它是个空字符串,则这个绑定会失效:可以利用这个特性来暂时禁用一些事件。

互斥事件绑定

自基础库版本 2.8.2 起,除 bindcatch 外,还可以使用 mut-bind 来绑定事件。一个 mut-bind 触发后,如果事件冒泡到其他节点上,其他节点上的 mut-bind 绑定函数不会被触发,但 bind 绑定函数和 catch 绑定函数依旧会被触发。

换而言之,所有 mut-bind 是“互斥”的,只会有其中一个绑定函数被触发。同时,它完全不影响 bindcatch 的绑定效果。

例如在下边这个例子中,点击 inner view 会先后调用 handleTap3handleTap2 ,点击 middle view 会调用 handleTap2handleTap1

<view id="outer" mut-bind:tap="handleTap1">
  outer view
  <view id="middle" bind:tap="handleTap2">
    middle view
    <view id="inner" mut-bind:tap="handleTap3">
      inner view
    </view>
  </view>
</view>

事件的捕获阶段

自基础库版本 1.5.0 起, 触摸与点击事件 支持捕获阶段。捕获阶段位于冒泡阶段之前,且在捕获阶段中,事件到达节点的顺序与冒泡阶段恰好相反。需要在捕获阶段监听事件时,可以采用 capture-bindcapture-catch 关键字,后者将中断捕获阶段和取消冒泡阶段。

在下面的代码中,点击 inner view 会先后调用 handleTap2handleTap4handleTap3handleTap1

<view id="outer" bind:touchstart="handleTap1" capture-bind:touchstart="handleTap2">
  outer view
  <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
    inner view
  </view>
</view>

如果将上面代码中的第一个 capture-bind 改为 capture-catch,将只触发 handleTap2

<view id="outer" bind:touchstart="handleTap1" capture-catch:touchstart="handleTap2">
  outer view
  <view id="inner" bind:touchstart="handleTap3" capture-bind:touchstart="handleTap4">
    inner view
  </view>
</view>

触发与监听事件

事件系统是组件间通信的主要方式之一。组件可以触发任意的事件,组件的使用者可以监听这些事件。

关于事件的基本概念和用法,参见 事件 。

监听事件

监听自定义组件事件的方法与监听基础组件事件的方法完全一致,例如:

<component-tag-name bind:myevent="onMyEvent" />
export default Page({
  onMyEvent: function(e){
    e.detail // 自定义组件触发事件时提供的 detail 对象
  }
})

触发事件

自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail 对象和事件选项,例如:

<button bind:tap="onTap"> 点击这个按钮将触发 myevent 事件 </button>
export default Component({
  properties: {},
  methods: {
    onTap: function(){
      const myEventDetail = {} // detail对象,提供给事件监听函数
      const myEventOption = {} // 触发事件的选项
      this.triggerEvent('myevent', myEventDetail, myEventOption)
    }
  }
})

触发事件的选项包括:

选项名 类型 是否必填 默认值 描述
bubbles Boolean false 事件是否冒泡
composed Boolean false 事件是否可以穿越组件边界,为 false 时,事件将只能在引用组件自身的 Shadow 树上触发,不进入其他任何组件内部
capturePhase Boolean false 事件是否拥有 捕获阶段

关于 bubblescomposed,可以参考下面这个例子。

<!-- 页面 page.wxml -->
<another-component bind:customevent="pageEventListener1">
  <my-component bind:customevent="pageEventListener2"></my-component>
</another-component>
<!-- 组件 another-component.wxml -->
<view bind:customevent="anotherEventListener">
  <slot />
</view>
<!-- 组件 my-component.wxml -->
<view bind:customevent="myEventListener">
  <slot />
</view>
// 组件 my-component.js
export default Component({
  methods: {
    onTap: function(){
      // 不冒泡
      // (只会触发 pageEventListener2)
      this.triggerEvent('customevent', {})

      // 在 Shadow 树上冒泡
      // (会依次触发 pageEventListener2、pageEventListener1)
      this.triggerEvent('customevent', {}, { bubbles: true })

      // 在 Composed 树上冒泡
      // (会依次触发 pageEventListener2、anotherEventListener、pageEventListener1)
      this.triggerEvent('customevent', {}, { bubbles: true, composed: true })
    }
  }
})

在独立任务中触发事件

triggerEvent 会同步触发事件。

有时候这会导致一些问题,例如,在属性 observer 或数据监听器中调用,可能会导致 递归更新问题 。此时,应该在一个独立的任务里触发事件,例如:

export default Component({
  properties: {
    foo: String,
  },
  observers: {
    foo() {
      // 如果直接在数据监听器中触发事件,可能会导致递归更新问题
      // 此时,可使用 nextTick 来在独立任务中触发事件
      wx.nextTick(() => {
        this.triggerEvent('customevent', {})
      })
    },
  },
})

虚拟组件节点

基础库 2.11.2 开始支持,低版本需做兼容处理。

默认情况下,自定义组件本身的那个节点是一个“普通”的节点,使用时可以在这个节点上设置 class style、动画、flex 布局等(就如同普通的 view 组件节点)。

<!-- 页面的 WXML -->
<view style="display: flex">
  <custom-component style="color: blue; flex: 1">蓝色、满宽的</custom-component>
</view>

启用组件节点虚拟化

有时,自定义组件并不希望这个节点本身响应 flex 布局,而是希望自定义组件内部的第一层节点能够响应 flex 布局。还有些时候,自定义组件不希望这个节点本身是一个“普通”可以设置样式的节点。

这种时候,可以将这个自定义组件设置为“虚拟的”:

export default Component({
  options: { virtualHost: true },
})

这样,可以将 flex item 放入自定义组件内部:

<!-- 页面的 WXML -->
<view style="display: flex">
  <!-- 如果设置了 virtualHost ,节点上的样式将失效 -->
  <custom-component style="color: blue">不是蓝色的</custom-component>
</view>
<!-- custom-component.wxml -->
<view style="flex: 1"> 满宽的 </view>

保留 class 和 style 能力

对于虚拟的组件节点,它的 class style 和动画将不再生效,但仍然可以:

  • style 定义成属性来获取 style 上设置的值;
  • class 定义成 外部样式类 使得自定义组件 WXML 可以使用 class
export default Component()
  .options({ virtualHost: true })
  .property('style', String)
  .externalClasses(['class'])
  .register()

引用外部样式

在组件样式隔离的前提下,有些组件希望接受组件使用者传入的样式类。

外部样式类

自基础库版本 1.9.90 起,可以用 externalClasses 定义若干个外部样式类。

这个特性可以用于实现类似于 view 组件的 hover-class 属性:页面可以提供一个样式类,赋予 viewhover-class,这个样式类本身写在页面的 WXSS 中而非 view 组件中。

/* foo-component.js */
export default Component({
  externalClasses: ['my-class'],
})
<!-- foo-component.wxml -->
<foo-component class="my-class">这段文本的颜色受组件引用者的 class 影响</foo-component>

这样,组件的使用者可以指定这个样式类对应的 class ,就像使用普通属性一样。在基础库版本 2.7.1 之后,可以指定多个对应的 class 。

/* 页面的 WXSS */
.red-text {
  color: red;
}
.large-text {
  font-size: 1.5em;
}
<!-- 页面的 WXML -->
<foo-component my-class="red-text" />
<foo-component my-class="red-text large-text" />

注意:在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的。可以借助选择器优先级机制来控制两个类的优先级。

多层嵌套传递外部样式类

外部样式类可以多层嵌套传递。例如,定义一个 bar-component 来引用上面的 foo-component

/* bar-component.js */
export default Component()
  .externalClasses(['bar-class'])
  .register()
<!-- bar-component.wxml -->
<foo-component my-class="bar-class" />

页面在引用 bar-component 时:

<!-- 页面的 WXML -->
<bar-component bar-class="red-text" />

此时 .red-text 会从页面经过 bar-component 最终传递到 foo-component 中。

直接引用父组件的样式

即使启用了样式隔离 isolated,组件仍然可以通过一些特殊语法来使用组件所在页面的样式类或父组件的样式类。

需要注意的是,这会增加组件间的不良耦合,优先推荐使用 外部样式类 来代替。

例如,如果在页面 WXSS 中定义了:

.blue-text {
  color: blue;
}

在这个组件中可以使用 ~ 来引用页面中这个类的样式:

<view class="~blue-text"> 这段文本是蓝色的 </view>

其中的 ~ 表示引用页面中对应名称的样式类。

另外,可以使用 ^ 表示引用父组件中对应名称的样式类;也可以连续使用多个 ^ 来引用祖先组件中的。

组件样式

考虑到组件间相互影响的问题,组件的样式表写法受到一定限制。

组件选择器限制

在编写组件样式表时,只推荐使用 class 选择器、伪类和伪元素选择器。这样比较利于代码维护,且它们具有更好的性能。

使用其他选择器需要注意:

  • 属性选择器([foo])不受支持、不会生效;
  • ID 选择器(#foo)不推荐使用,仅在使用 Page 构造器构造的页面根组件中生效;
  • 子元素选择器(.foo > .bar)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况;
  • 标签名选择器(button)应尽量避免使用,它不受样式隔离限制,对所有组件都会生效;如需使用,应写在 app.wxss 中。
[foo] { } /* 不受支持 */
#foo { } /* 不推荐  */
.foo > .bar { } /* 除非 .foo 是 view 组件节点,否则不一定会生效 */
button { } /* 不推荐 */

组件样式隔离

默认情况下,自定义组件的样式只受到自定义组件 WXSS 样式的影响,即 样式隔离 。这样,可以在自定义组件中使用比较短的 class 名字,无需担心与其他组件的 class 名冲突。

<view class="title">标题</view>
.title {
  font-size: 2em;
}

从基础库版本 2.10.1 开始,可以通过设置 styleIsolation 选项来修改自定义组件样式隔离行为。它支持以下取值:

  • isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响;
  • apply-shared 表示页面的样式将影响到自定义组件,但自定义组件的样式不会影响页面;
  • shared 表示页面的样式将影响到自定义组件,自定义组件的样式也会影响页面和其他设置了 apply-sharedshared 的自定义组件(这个选项在插件的自定义组件中不可用)。

styleIsolation 选项需要在自定义组件 JSON 中设置:

{
  "usingComponents": {},
  "styleIsolation": "isolated"
}

对于 styleIsolation 的默认值:

  • 如果组件是 Page 构造器构造的页面根组件,styleIsolation 的默认值为 shared
  • 如果组件路径在 app.jsonpages 列表中,styleIsolation 的默认值为 shared
  • 在其他组件中,styleIsolation 的默认值为 isolated

styleIsolation 也可以在 JS 脚本的 options 中配置,但目前已不推荐。

JS 脚本的 options 中也支持 addGlobalClass: true 配置,等价于设置 styleIsolation: apply-shared。目前已不推荐使用这个配置。

host 节点选择器

自基础库 1.7.2 起,通过 :host 选择器,组件可以指定它自身对应节点的默认样式。例如:

/* custom-component.wxss */
:host {
  color: yellow;
}

无论引用者为组件赋予的节点名是什么,:host 选择器都会对这个节点生效。例如:

{
  "usingComponents": {
    "custom": "/path/to/custom-component"
  }
}
<!-- 引用组件的 WXML -->
<custom>这段文本是黄色的</custom>

behaviors

behaviors 是用于组件间脚本代码共享的特性,类似于一些编程语言中的 mixins

Behavior 构造器

behaviors 通过 Behavior 构造器构造。

类似于 Component 构造器,Behavior 构造器也有 Definition 和 Chaining 两种形式,也可以混用。

Behavior 构造器的 Definition 形式例如:

const fooBehavior = Behavior({
  data: {
    foo: 1,
  },
  lifetimes: {
    attached() {
      // 页面创建时执行
    },
  },
  methods: {
    onButtonTap() {
      // 用户事件触发时执行
    },
  },
})

Chaining 形式例如:

const fooBehavior = Behavior()
  .data(() => ({
    foo: 1,
  }))
  .init(function ({ lifetime, method }) {
    lifetime('attached', () => {
      // 页面创建时执行
    })
    const buttonTap = method(() => {
      // 用户事件触发时执行
    })
    return {
      buttonTap,
    }
  })
  .register()

Behavior 构造器的用法和 Component 构造器非常相似,只是 options / .options() 对于 Behavior 构造器无效。另外,Behavior 构造器的返回值需要用到,需要把返回值存储到变量中或 export 给别的文件使用。

使用 behaviors

Component 构造器中,可以通过 behaviors / .behavior() 来引用 behaviors。

Definition 形式引用 behaviors 例如:

export default Component({
  behaviors: [fooBehavior],
  data: {
    bar: 2,
  },
})

Chaining 形式引用 behaviors 例如:

export default Component()
  .behavior(fooBehavior)
  .data(() => ({
    bar: 2,
  }))
  .register()

引用 behaviors 后,相当于将 behaviors 中的属性、数据字段、方法、生命周期函数等等都合并到组件中。在上例中,fooBehavior 提供了 foo 数据字段,而组件自身有 bar 数据字段,所以整个组件就有 foobar 两个数据字段。属性、方法、生命周期函数等等也会产生类似的效果。

一个组件可以引用多个 behaviors,而且对它们的定义形式没有要求(可以一些是 Definition 形式定义的、另一些 Chaining 形式定义的)。

Behavior 构造器中也可以嵌套引用其他 behaviors。

const barBehavior = Behavior()
  .behavior(barBehavior)
  .data(() => ({
    bar: 2,
  }))
  .register()

微信小程序基础库版本 2.9.2 起,在 Page 构造器中,也可以类似地使用 behaviors

export default Page({
  behaviors: [myBehavior],
})

同名字段的覆盖和组合规则

组件和它引用的 behaviors 中可以包含同名的字段。

如果有同名的属性 properties 或方法 methods 是覆盖式的:

  • 若组件本身有这个属性或方法,则组件的属性或方法会覆盖 behaviors 中的同名属性或方法;
  • 若组件本身无这个属性或方法,则在组件 behaviors 中靠后的会覆盖靠前的;
  • 若存在嵌套引用 behaviors 的情况,则引用者的属性或方法覆盖被引用的。

如果有同名的数据字段:

  • 若同名的数据字段都是对象类型,会进行对象合并;
  • 其余情况处理方式是覆盖式的(与上述属性和方法的方式一致)。

数据监听器 observers、生命周期函数 lifetimes、页面生命周期函数 pageLifetimes、外部样式类 externalClasses 等等其他情况,都是组合式的。例如,在 behaviors 和组件自身都定义了同一个生命周期函数时,它们都会被执行,执行顺序是:

  • 先按 behaviors 列表中的顺序执行(如果定义了);
  • 最后执行组件自身的生命周期函数(如果定义了);
  • 若存在嵌套引用 behaviors 的情况,则被引用的优先于引用者的执行;

如果同一个 Behavior 被一个组件多次引用,它的数据监听器、生命周期函数、页面生命周期函数可能会被重复执行(也可能不会),基于以下规则:

  • 如果是 Behavior 是 Definition 形式的,那它的数据监听器、生命周期函数、页面生命周期函数 不会 被重复执行;
  • 如果是 Behavior 是 Chaining 形式的,那它的数据监听器、生命周期函数、页面生命周期函数 被重复执行。

内置 behaviors

微信小程序基础库也定义了一些内置的 behaviors。自定义组件可以通过引用它们来获得内置组件的一些行为。

export default Component()
  .behavior('wx://form-field')
  .register()

在上例中, wx://form-field 是内置的,它使得这个自定义组件有类似于表单控件的行为。

内置 behaviors 往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type 或添加 observer

wx://form-field

使自定义组件有类似于表单控件的行为。 form 组件可以识别这些自定义组件,并在 submit 事件中返回组件的字段名及其对应字段值。

详细用法以及代码示例请参考 form 组件参考文档 。

wx://form-field-group

从基础库版本 2.10.2 开始提供支持。

使 form 组件可以识别到这个自定义组件内部的所有表单控件。

详细用法以及代码示例请参考 form 组件参考文档 。

wx://form-field-button

从基础库版本 2.10.3 开始提供支持。

使 form 组件可以识别到这个自定义组件内部的 button。如果自定义组件内部有设置了 form-type 的 button,它将被组件外的 form 接受。

详细用法以及代码示例请参考 form 组件参考文档 。

wx://component-export

仅对 exparser 有效,glass-easel 组件框架自带这个特性,不需要额外引入。

从基础库版本 2.2.3 开始提供支持。

使自定义组件支持 export 定义段。这个定义段可以用于指定组件被 selectComponent 调用时的返回值。

组件生命周期

普通生命周期

微信小程序组件发生一些关键状态变化的时候,比如被添加到页面中、从页面中移除等,会触发对应的生命周期回调函数。

其中,最重要的生命周期是 attached。它表示组件实例被添加到页面内;对于页面根组件,它表示整个页面创建完成并即将被展示。

Definition 形式的生命周期回调函数:

export default Component({
  lifetimes: {
    // attached 生命周期回调函数
    attached() {
      // 添加到页面时触发
    },
  },

  // 传统写法(不推荐)
  attached() {
    // 添加到页面时触发
  },
})

Chaining 形式的生命周期回调函数:

export default Component()
  .lifetimes('attached', function () {
    // ...
  })
  .register()

目前提供的生命周期回调函数如下表所示。

生命周期 触发时机 触发次数 注意事项
created 组件实例刚刚被创建完时触发 每个实例触发一次 组件还未添加到页面节点树中,不能通过组件节点向上查找父节点或其他兄弟节点。
attached 组件实例被添加到页面后触发 每个实例最多触发一次
moved 组件实例在节点树中位置被移动后触发 次数不定 只有 wx:for 内项目可能触发。
beforeDetach 组件实例将被从页面内移除前触发 每个实例最多触发一次 组件将被移除,不再位于节点树中,不应再操作节点或更新数据。
detached 组件实例被从页面内移除后触发 每个实例最多触发一次 组件已被移除,不再位于节点树中,不应再操作节点或更新数据。
ready 组件准备就绪时触发 每个实例最多触发一次 表示组件在渲染线程完全渲染完毕
error 组件内的生命周期回调或事件回调抛出异常时触发 次数不定 回调参数为 (err: unknown) ,可用于组件级别的错误捕获

注意,ready 生命周期不推荐使用。因为它的回调触发可能会非常晚,并可能在组件被移除后才回调。绝大多数情况下,应使用 attached 代替。使用 ready 生命周期时,需要处理以下可能的情况:

  • attached 触发后,ready 被触发;
  • attached 未触发,仅 ready 被触发;
  • attacheddetached 依次触发后,ready 被触发。

除此以外,还有一些传统的页面生命周期回调函数,可以在页面根组件中使用。页面路由文档中详细说明了部分生命周期的触发时序。

组件所在页面的生命周期

页面生命周期是一类特殊的生命周期。它在某个页面上触发时,会自动广播给所有页面内的所有组件。

生命周期 参数 描述 最低版本
show 组件所在的页面被展示时执行 2.2.3
hide 组件所在的页面被隐藏时执行 2.2.3
resize Object Size 组件所在的页面尺寸变化时执行 2.4.0
routeDone 组件所在页面路由动画完成时执行 2.31.2

注意:自定义 tabBar 的 pageLifetime 不会触发。

Definition 形式的组件所在页面生命周期回调函数:

Component({
  pageLifetimes: {
    show: function () {
      // 页面被展示时触发
    },
  },
})

Chaining 形式的组件所在页面生命周期回调函数:

Component()
  .pageLifetime('show', function () {
    // 页面被展示时触发
  })
  .register()