组件间交互概述

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

父组件与子组件间的通信

两个有引用与被引用关系的组件,通常可以称为父组件和子组件。父组件通过 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()

组件属性

组件可以定义一些属性,用来接收组件使用者传入的值。组件的使用者可以在 WXML 节点中指定这些属性对应的值。

定义属性

使用 Definition 形式定义属性,写法如下:

export default Component({
  properties: {
    fooProp: {
      type: String,
      value: '',
    },
  },
})

使用 Chaining 形式定义属性,写法如下:

export default Component()
  .property('fooProp', {
    type: String,
    value: '',
  })
  .register()

上面的例子中定义了 fooProp 属性,定义时提供的 type 表示属性类型,value 表示初始值。

定义好属性之后,组件的使用者就可以在 WXML 节点中指定属性对应的值了,比如在页面中可以这样写:

<!-- page/index/index.wxml -->
<custom-component foo-prop="bar" />

在 WXML 中,驼峰写法的属性名可以换成连字符写法(当然也可以继续使用驼峰写法)。

给组件属性命名时,需要注意以下几点:

  • 属性名不能和 data 数据字段重名;
  • id class style slot 是 WXML 的保留属性,虽然可以设为组件属性,但无法在 WXML 中对它们赋值;
  • dataXxx 这样的属性名也无法在 WXML 中对它们赋值,因为 data-xxx="" 会被视为 dataset。

属性类型与简化定义

属性支持下表中列出的类型。

类型 默认值 说明
String '' 字符串
Number 数值
Boolean false 布尔值
Object null 对象
Array [] 数组
Function function () {} 函数
null null 表示属性支持任意类型

Function 类型仅 glass-easel 组件框架支持。在 exparser 组件框架中不能传递函数类型的属性,但可以将函数作为 Object 类型中的一个字段来传递。

如果一个属性的初始值 value 和它的类型默认值相同,就可以把属性定义简写为 type 本身,例如:

export default Component({
  properties: {
    fooProp: String,
  },
})
export default Component()
  .property('fooProp', String)
  .register()

初始值和默认值

初始值 指的是一个属性没有被指定值时所具有的值。

默认值 指的是一个属性没有被指定值时所具有的值,或者当属性被赋予非法值时,用来代替非法值的那个属性值。仅 glass-easel 组件框架支持。默认值需要使用一个函数来指定,写法如下:

export default Component({
  properties: {
    fooProp: {
      type: String,
      default: () => 'DEFAULT',
    },
  },
})
export default Component()
  .property('fooProp', {
    type: String,
    value: () => 'DEFAULT',
  })
  .register()

如果指定了默认值 default,就不能再指定初始值 value 了。

属性值类型转换

在 WXML 中为一个属性指定值时,会自动进行类型转换;如果转换失败,就会被视为非法值(这时会使用默认值来代替)。typenull 的属性不会进行类型转换。

举个例子,如果 custom-component 组件的 num-prop 属性是 Number 类型的,而在使用者的 WXML 中这样写:

<custom-component num-prop="1" />

这种情况下,num 属性会接收到数值 1

布尔类型属性值转换

对于 Boolean 类型的属性,如果在 WXML 中只写了属性名而没有指定值,会自动把它的值设为 true

<custom-component bool-prop />
<!-- 等价于 -->
<custom-component bool-prop="ANY" />
<!-- 等价于 -->
<custom-component bool-prop="{{true}}" />

如果想把它的值设为 false,就必须使用数据绑定写法,例如:

<custom-component bool-prop="{{false}}" />

对象、数组类型属性值转换

对象和数组类型的属性只能使用数据绑定写法,例如:

<custom-component array-prop="{{ [1, 2] }}" object-prop="{{ { a: 1, b: 2 } }}" />

在 glass-easel 组件框架中,绑定对象时外层的花括号可以省略:

<custom-component object-prop="{{ a: 1, b: 2 }}" />

可选类型

如果一个属性可能接受多种类型的值,可以用 optionalTypes 为它附加多个类型。例如:

export default Component({
  properties: {
    fooProp: {
      type: String,
      optionalTypes: [String, Boolean],
    },
  },
})
export default Component()
  .property('fooProp', {
    type: Number,
    optionalTypes: [String, Boolean],
  })
  .register()

使用 TypeScript 指定对象类型

使用 TypeScript 时,对于 typeArrayObject 的属性,会根据它初始值、默认值的类型作为这个属性字段的具体类型,来进行类型推断。

这时,可以通过 as 来指定具体类型:

export default Component()
  .property('objectProp', {
    type: Object,
    value: {} as {
      foo?: number
      bar?: string
    },
  })
  .register()

属性值变化监听器

属性值可以指定一个 observer,当属性值发生变化时,会调用这个函数。

export default Component()
  .property('foo', {
    type: String,
    observer(newVal, oldVal) {
      // 属性值变化后触发
    },
  })
  .register()

如果需要在 observer 中同步执行 setData 等更新操作,更推荐使用数据监听器,因为通常它有更好的性能。

对于 type 为 ObjectArray 的属性,如果通过该组件自身的 this.setData 来改变属性值的一个子字段,则依旧会触发属性 observer,且 observer 接收到的 newVal 是变化的那个子字段的值。

属性值比较器

仅 glass-easel 组件框架支持。

每当组件的使用者进行更新时,组件框架需要判断它使用的每个子组件是不是被更新了。框架的判断方法是,对比每个属性值,如果属性值不一样,就认为子组件被更新了。如果子组件被更新了,就会产生一定的更新开销。

对于子组件属性是数值、字符串、布尔值等基本类型时,框架可以直接对比它们的值。但对于对象、数组这样的值,框架难以比较,常常将它们直接视为被更新了。

这时可以通过 comparer 来指定一个属性值比较函数,避免不必要的更新、提升整体性能。如果 comparer 返回真,表示属性值变化了、组件需要更新。例如:

export default Component()
  .property('objectProp', {
    type: Object,
    comparer(newValue, oldValue) {
      return newValue.foo !== oldValue.foo || newValue.bar !== oldValue.bar
    },
  })
  .register()

页面根组件的属性值

对于页面根组件,属性值会从页面的 query 参数中获取(参数值会被自动 URL decode)。例如:

// pages/index/index.js
export default Component()
  .property('strProp', String)
  .property('numProp', Number)
  .register()

如果页面以 pages/index/index?strProp=foo&numProp=1 打开,那么 strProp 会被赋值 'foo'numProp 会被赋值 1

由于 query 参数是字符串,页面根组件的属性值应当是 StringNumber 类型的。