纯数据字段

纯数据字段是一些 WXML 模板中没用到的 data 字段。从微信小程序基础库版本 2.8.2 开始支持。

在 exparser 组件框架中,使用纯数据字段有助于提升整体性能。在 glass-easel 组件框架中,使用纯数据字段对整体性能的影响很小。

如果使用 Chaining 形式,不用于 WXML 模板的数据应优先考虑使用 init 函数内的局部变量代替(这样也就用不上纯数据字段了)。

export default Component()
  .data(() => ({
    // data 中包含的数据应当是用于 WXML 模板的
    dataUsedInTemplate: true,
  }))
  .init(function () {
    // 不用于 WXML 模板的数据就写成局部变量
    const dataNotUsedInTemplate = {}
  })
  .register()

组件数据中的纯数据字段

指定纯数据字段的方法是在 Component 构造器的 options 定义段中指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。

在开发者工具中预览效果

代码示例:

export default Component({
  options: {
    pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
  },
  data: {
    a: true, // 普通数据字段
    _b: true, // 纯数据字段
  },
  methods: {
    myMethod() {
      this.data._b // 纯数据字段可以在 this.data 中获取
      this.setData({
        c: true,
        _d: true, // 纯数据字段也可以被 setData 设置
      })
    },
  },
})

上述组件中的纯数据字段不会被应用到 WXML 上:

<view wx:if="{{a}}"> 这行会被展示 </view>
<view wx:if="{{_b}}"> 这行不会被展示 </view>

虽然不能用于 WXML,但纯数据字段可以使用 数据监听器 来监听变化。

组件属性中的纯数据字段

属性也可以被指定为纯数据字段(遵循 pureDataPattern 的正则表达式)。属性中的纯数据字段可以像普通属性一样接收外部传入的属性值,但不能将它直接用于组件自身的 WXML 中。

注意:属性中的纯数据字段的属性 observer 永远不会触发!如果想要监听属性值变化,使用 数据监听器 代替。

export default Component({
  options: {
    pureDataPattern: /^_/
  },
  properties: {
    a: Boolean,
    _b: {
      type: Boolean,
      observer() {
        // 不要这样做!这个 observer 永远不会被触发
      }
    },
  }
})

微信小程序基础库版本 2.10.1 开始,也可以在组件的 json 文件中配置 pureDataPattern(这样就不需在 js 文件的 options 中配置)。此时,其值应当写成字符串形式:

{
  "pureDataPattern": "^_"
}

组件初始化策略

仅 glass-easel 组件框架支持。

组件实例初始化流程有两种。

第一种初始化流程是 以组件初始值来初始化 ,具体流程是:

  1. 以组件自己定义的数据应用到 WXML 模板上;
  2. 触发 created 生命周期;
  3. 应用从 WXML 传来的属性值(对于页面根组件,就是从页面的 query 传来的属性值);
  4. 如果改变的属性会触发的数据监听器,那就把触发对应的数据监听器;
  5. 将改变后的属性值应用到 WXML 模板上。

第二种初始化流程是 以组件属性值来初始化 ,具体流程是:

  1. 应用从 WXML 传来的属性值(对于页面根组件,就是从页面的 query 传来的属性值);
  2. 如果改变的属性会触发的数据监听器,那就把触发对应的数据监听器;
  3. 将组件自己定义的数据连同改变后的属性值一同应用到 WXML 模板上;
  4. 触发 created 生命周期。

组件的后续逻辑(如触发 attached 生命周期等),在这两种初始化流程上没有区别。下表是对两种初始化流程的主要区别的对比。

比较项 以组件初始值来初始化 以组件属性值来初始化
created 生命周期触发时的属性值 组件自己定义的属性初始值 从 WXML(或页面 query)传来的属性值
数据监听器先于 created 触发? 不可能 有可能
性能 较差 较好

glass-easel 目前以前者为默认的初始化方式,这种初始化方式具有更单一的时序,但是性能会相对差一些(因为可能多一次 WXML 模板更新)。

如果想追求更好的性能,可以设置 propertyEarlyInit 选项来改为后者:

export default Component()
  .options({
    // 以组件属性值来初始化
    propertyEarlyInit: true,
  })
  .data(() => ({
    a: 1,
  }))
  .init(function ({ lifetime, observer }) {
    observer('a', () => {
      // 可能早于 created 触发
    })
    lifetime('created', () => {
      // 可能晚于 observer 触发
    })
  })
  .register()

数据字段拷贝控制

仅 glass-easel 组件框架支持。

组件框架在部分时候会对数据进行 深拷贝。会执行深拷贝的时刻包括:

  • 使用 setData 等数据更新方法的时候;
  • 在组件间传递属性值的时候;
  • 向 WXS 传递参数、WXS 返回值的时候。

其中前两者的深拷贝行为可以通过组件选项来调整。(目前 WXS 相关的深拷贝行为不能调整。)

数据深拷贝

使用 setData 等数据更新方法的时候,默认情况下,数据字段应用到 WXML 模板前会经过一次深拷贝。这样,对 this.data 的直接修改就不会应用到 WXML 模板上。例如:

// 这样设置的 foo 不会应用到 WXML 模板上
this.data.foo = 'bar'

因为实际应用在 WXML 模版上的数据是 this.data 的一份深拷贝副本。如果直接修改 this.data,它的深拷贝副本不会更新。

简单深拷贝

默认情况下,这是一种 简单深拷贝 策略:递归复制所有基本 JS 类型、数组、简单对象、函数和 Symbol,即,所有 JSON 可表达的数据类型外加上函数、Symbol。它不会递归复制:

  • 数组中使用非 number 索引的字段(会丢失);
  • 函数中的子字段(不会复制、会保留同一引用);
  • 对象的原型(会丢失);
  • Date RegExp 等 JS 内置对象(会视为普通对象处理,丢失原型)。

支持子字段递归的深拷贝

如果对象内的子字段存在无限递归的情况,例如:

var rec = { b: null }
rec.b = { c: rec }

// 此时
rec.b.c === rec // true
rec.b.c.b.c === rec // true
rec.b.c.b.c.b.c === rec // true
// ...
// rec 对象的子字段会出现无限递归

// setData 时,会触发栈溢出!
this.setData({ foo: rec }) // 不要这么做!

这会导致深拷贝时出现无限递归导致的栈溢出。出现这种情况时,需要将深拷贝策略改为 支持子字段递归的深拷贝 simple-recursion

export default Component()
  .options({
    // 将深拷贝策略改为支持子字段递归的深拷贝
    dataDeepCopy: 'simple-recursion',
  })
  .init(function ({ setData, lifetime }) {
    lifetime('attached', () => {
      setData({ foo: rec }) // OK!
    })
  })
  .register()

禁用深拷贝

也可以选择完全禁用深拷贝,前提是你确定不会直接修改 this.data。这样可以完全避免深拷贝带来的开销、对象的原型链能够保留、性能最优:

export default Component()
  .options({
    // 禁用深拷贝
    dataDeepCopy: 'none',
  })
  .init(function ({ setData, lifetime }) {
    lifetime('attached', () => {
      setData({
        foo: rec,
        date: new Date(),
      })
    })
  })
  .register()

所有可用的深拷贝策略

下表包含了目前所有可用的深拷贝策略。

深拷贝策略 说明
simple 启用简单深拷贝策略(默认值)
simple-recursion 启用支持子字段递归的深拷贝
none 禁用深拷贝

属性传递深拷贝

默认情况下,通过 WXML 传递组件属性值时,也会经过一次深拷贝。这样可以使得两个组件之间不共享对象,对一个组件的数据变更不会影响另一个组件。

但这也会让属性传递时不能传递存在无限递归的对象,也会有一定的深拷贝开销。可以通过更改被引用组件的 propertyPassingDeepCopy 选项来选用不同的深拷贝策略。

例如,在被引用组件(子组件)上:

// custom-component.js
export default Component()
  .options({
    propertyPassingDeepCopy: 'simple-recursion',
  })
  .property('fooProp', Object)
  .register()

这样,子组件的所有属性就都可以接受无限递归的对象了。

propertyPassingDeepCopy 也可以指定为 none 来禁用深拷贝。禁用之后,多个组件的数据中将共享同一对象的引用,所以在编程时请格外留意:避免修改数组、对象属性的子字段。

// custom-component.js
export default Component()
  .options({
    propertyPassingDeepCopy: 'none',
  })
  .property('fooProp', {
    type: Object,
    value: {
      bar: 1,
    },
  })
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      const foo = this.data.fooProp
      // 请留意不要修改属性的子字段
      foo.bar = 2 // 不要这样做!
    })
  })
  .register()

在组件间无拷贝地传递复杂对象

有时,需要在组件间共享带有原型链的复杂对象,或完全避免巨大数组、巨大对象的拷贝开销。这时需要进行两项设置:

  • 在被引用组件(子组件)上,设置 propertyPassingDeepCopynone
  • 在引用组件(父组件)上,设置 dataDeepCopynone

这样,父组件就可以通过 setData 设置对象、数组,通过 WXML 模板传递后,子组件会无拷贝原样收到。

数据更新策略

在将组件数据应用到 WXML 模板上时,组件框架会用到一些更新算法。

数据更新算法

目前,组件框架可能使用两种更新算法:虚拟树更新、绑定映射表更新。

虚拟树更新

在执行虚拟树更新时,需要对整个 Shadow 树进行深度优先遍历,找到哪些数据绑定需要更新。

本质上来说,这是个深度优先遍历的过程,它的算法性能与 Shadow 树中的节点总量、节点复杂程度显著正相关。想要提升性能,建议合理划分组件、减少 Shadow 树中的节点数量。

绑定映射表更新

在 glass-easel 组件框架中,如果只是对个别数据字段进行小更新,组件框架会尝试使用绑定映射表更新。这种更新方式不需要遍历组件树,而是依靠一些编译期的额外信息来直接找到需要更新的数据绑定表达式。在更新的数据量比较小时,这样会显著提升性能。

然而,组件框架并不能高效判断哪种更新方式最合适。所以,目前 glass-easel 组件框架会基于如下策略来决定某次更新采用的算法。

  1. 如果满足以下所有条件,那就采用绑定映射表更新:
  • 单独更新一个数据字段;
  • 这个数据字段没有被用于 wx:if wx:for let: 节点及它们的子孙节点。
  1. 否则,采用虚拟树更新。

子组件更新的触发

无论采用哪种更新算法,它们的更新范围都在当前组件实例 Shadow 树范围内。

但在此过程中,如果某个自定义组件的属性被更新了,那会继而触发这个自定义组件实例的数据更新(递归地)。对于这个自定义组件实例 Shadow 树,组件框架会再次选用合适的数据更新算法来更新它。

对于组件 ObjectArray 类型的属性,组件框架可能无法很好判断它们是不是被更新了。此时建议使用 属性值比较器 来辅助框架判断。

WXS 的运行方式

对于 WXML 模板中包含的 WXS 代码,不同组件框架的运行方式完全不同。

在 glass-easel 组件框架中,WXS 代码运行在逻辑层中。在每次更新时,组件框架会判断有哪些数据绑定表达式需要更新,只有这些数据绑定表达式内的 WXS 函数会被调用。例如:

<wxs module="foo">
  exports.f1 = function (a) {
    return a
  }
  exports.f2 = function (a) {
    return a
  }
</wxs>

<view> {{ foo.f1(s) }} </view>
<view> {{ foo.f2(t) }} </view>

对于上面这个 WXML 模板,如果 setData 更新字段中只有 s 而没有 t,那么只有 f1 会被调用,f2 不会被调用。

在 exparser 组件框架中,WXS 代码运行同时运行在逻辑层和视图层中。每次更新时,组件框架会将 Shadow 树中的 WXS 函数分别在逻辑层和视图层各执行一次。

此外,无论对于哪个组件框架,WXS 事件响应函数 都是仅运行在视图层中的,与其它 WXS 代码全局量并不互通。

高级数据更新方法

组件框架支持几种更新数据的方式。

使用 setData 更新数据

setData 是最经典的更新数据方法。在调用 setData 时,组件框架会做两件事情:

  1. 立刻将变更的数据改到组件数据(即 this.data)上;
  2. 将新的数据应用到 WXML 模板上、更新界面。

setData 对 WXML 模板的更新是 同步 的(除了在数据监听器内被调用时)。换而言之,调用完 setData 后,可以视为 WXML 立刻被更新完毕了,可以立刻调用 组件树访问 和 获取界面上的节点信息 等相关方法。

除了最普通的用法外,setData 还可以使用数据路径来更新 data 中的某个子数据字段,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ setData, lifetime }) {
    lifetime('attached', () => {
      // 将 data.obj.a[0] 设置为 3
      setData({
        'obj.foo[0]': 3,
      })
    })
  })
  .register()

setData 的第二个参数(可选)是一个回调函数,它在这组数据完全应用到渲染引擎后回调。这个回调函数的回调时机偏晚,滥用可能会影响性能,请谨慎使用。

this.setData({}, () => {
  // 完全应用到渲染引擎后回调
})

高级路径更新

如果想仅更新对象内的一个字段,可以使用高级路径更新的方式。这样虽然接口复杂一些,但可以拥有更好的性能。

可以使用 replaceDataOnPath 来更新对象内的数据字段,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 将 data.obj.a[0] 设置为 3
      // (等价于 setData 'obj.foo[0]')
      this.replaceDataOnPath(['obj', 'foo', 0], 3)
      // 将更新应用到模板上
      this.applyDataUpdates()
    })
  })
  .register()

spliceArrayDataOnPath 仅 glass-easel 组件框架支持。

如果字段是数组类型的,还可以使用 spliceArrayDataOnPath 来对数组项进行插入和删除,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2, 3, 4],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 类似于数组的 splice 方法
      // 可以在一个位置上移除若干项,再插入若干项
      this.spliceArrayDataOnPath(['obj', 'foo'], 1, 2, [5, 6, 7])
      // 得到的 obj.foo 是 [1, 5, 6, 7, 4]
      // 将更新应用到模板上
      this.applyDataUpdates()
    })
  })
  .register()

调用 replaceDataOnPathspliceArrayDataOnPath 后,并不会立即将更新内容应用到 WXML 模板上,还需要调用 applyDataUpdates。如果需要连续调用多个 replaceDataOnPathspliceArrayDataOnPath,在末尾调用一次 applyDataUpdates 即可。

组合更新

groupUpdates 仅 glass-easel 组件框架支持。

连续调用多个 replaceDataOnPathspliceArrayDataOnPath 后,可能会遗忘调用 applyDataUpdates

可以考虑改用 groupUpdates 将它们组合起来,例如:

export default Component()
  .data(() => ({
    obj: {
      foo: [1, 2],
    },
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      this.groupUpdates(() => {
        this.replaceDataOnPath(['obj', 'foo', 0], 3)
        this.replaceDataOnPath(['obj', 'foo', 1], 4)
      })
    })
  })
  .register()

groupUpdates 回调函数返回后,会自动将更新应用到 WXML 模板上,不再需要调用 applyDataUpdates

使用 updateData 更新

updateData 仅 glass-easel 组件框架支持。

updateData 调用语法类似于 setData,但它不会立刻将更新应用到 WXML 模板上,需要额外调用 applyDataUpdatesgroupUpdates

如果需要连续多次调用 setData,推荐改用 updateData,并结合 groupUpdates 来将它们组合起来。这种方式性能显著优于多次调用 setData。例如:

export default Component()
  .data(() => ({
    a: 1,
    b: 2,
  }))
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      // 用 groupUpdates 将多个 updateData 组合起来
      this.groupUpdates(() => {
        this.updateData({ a: 3 })
        this.updateData({ b: 4 })
      })
      // 这样做的性能优于连续多次调用 setData
    })
  })
  .register()

此外,在 数据监听器 内使用 setData 时,推荐使用 updateData 代替:因为数据监听内的 setData 相当于 updateData

使用 groupSetData 连续更新

有时候,连续两个 setData 不能被替换为 groupUpdates + updateData。典型的情况是,第一个 setData 更新 <scroll-view> 内部的节点列表,第二个 setData 需要更新滚动位置。这种时候必须用两个 setData 来分别更新。

但这样可能导致第一次 setData 更新后、第二次 setData 更新前的界面中间状态展示给用户。此时可以用 groupSetData 将两个 setData 组合起来,确保界面中间状态不要展示给用户。用法例如:

this.groupSetData(() => {
  this.setData({ list })
  this.setData({ scrollPosition })
})

递归更新问题

调用 setDataapplyDataUpdates,或当 groupUpdates 回调函数执行完毕的时刻,新的组件数据应用到 WXML 模板上。这是个同步的过程。

在这个同步过程中,更新执行到一半的时候,可能先暂停父组件的更新、进入到子组件内部、让子组件先执行完更新,再回到父组件继续更新。如果此时,子组件又通过某些方法再次触发了父组件的更新,会导致父组件同时进行两个更新,会导致 WXML 模版混乱。

最常见的一种情况是:

  1. 父组件更新了子组件的某个属性;
  2. 子组件触发了 数据监听器 或 属性 observer ;
  3. 在监听器回调里面触发事件;
  4. 父组件又同步调用了一次 setData(或其他数据更新方法)。

避免这种问题的方法是:适当使用 wx.nextTick 避免递归更新,可参考 在独立任务中触发事件 的做法。

数据监听器

数据监听器可以用于监听和响应任何数据字段的变化。从微信小程序基础库版本 2.6.1 开始支持。

数据监听器和属性的 observer 相比,数据监听器更强大且通常具有更好的性能。

使用数据监听器

有时,在一些数据字段被 setData 设置时,需要执行一些操作。

例如,this.data.sum 永远是 this.data.numberAthis.data.numberB 的和。在 Definition 形式可以这样使用:

export default Component({
  data: {
    numberA: 0,
    numberB: 0,
  },
  lifetimes: {
    attached: function() {
      this.setData({
        numberA: 1,
        numberB: 2,
      })
    },
  },
  observers: {
    'numberA, numberB': function(numberA, numberB) {
      // 在 numberA 或者 numberB 被设置时,执行这个函数
      // 在 exparser 组件框架中
      this.setData({
        sum: numberA + numberB
      })
    }
  }
})

注意:在数据监听器中调用 setData 后,数据变更并不会同步应用到 WXML 模板上!换而言之,这里调用 setData 的效果相当于 updateData;若使用 glass-easel 组件框架,出于表意明确的考虑,在这里推荐写成 updateData

在 Chaining 形式可以这样写:

export default Component()
  .data(() => ({
    numberA: 0,
    numberB: 0,
    sum: 0,
  }))
  .init(function ({ setData, lifetime, observer }) {
    lifetime('attached', () => {
      setData({
        numberA: 1,
        numberB: 2,
      })
    })

    // 在 numberA 或者 numberB 被设置时,执行这个函数
    observer(['numberA', 'numberB'], (numberA, numberB) => {
      this.updateData({
        sum: numberA + numberB,
      })
    })
  })
  .register()

监听字段语法

数据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个监听器一次。

同时,监听器可以监听子数据字段,如下例所示。

export default Component({
  observers: {
    'some.subfield': function(subfield) {
      // 使用 setData 设置 this.data.some.subfield 时触发
      // (除此以外,使用 setData 设置 this.data.some 也会触发)
      subfield === this.data.some.subfield
    },
    'arr[12]': function(arr12) {
      // 使用 setData 设置 this.data.arr[12] 时触发
      // (除此以外,使用 setData 设置 this.data.arr 也会触发)
      arr12 === this.data.arr[12]
    },
  }
})
export default Component()
  .init(function ({ observer }) {
    observer('some.subfield', (subfield) => {
      // 使用 setData 设置 this.data.some.subfield 时触发
      // (除此以外,使用 setData 设置 this.data.some 也会触发)
      subfield === this.data.some.subfield
    })
    observer('arr[12]', (arr12) => {
      // 使用 setData 设置 this.data.arr[12] 时触发
      // (除此以外,使用 setData 设置 this.data.arr 也会触发)
      arr12 === this.data.arr[12]
    })
  })
  .register()

如果需要监听所有子数据字段的变化,可以使用通配符 **

export default Component({
  observers: {
    'some.field.**': function(field) {
      // 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发
      // (除此以外,使用 setData 设置 this.data.some 也会触发)
      field === this.data.some.field
    },
  },
  lifetimes: {
    attached: function() {
      // 这样会触发上面的 observer
      this.setData({
        'some.field': { /* ... */ }
      })
      // 这样也会触发上面的 observer
      this.setData({
        'some.field.xxx': { /* ... */ }
      })
      // 这样还是会触发上面的 observer
      this.setData({
        'some': { /* ... */ }
      })
    },
  },
})

特别地,仅使用通配符 ** 可以监听全部 setData 。

export default Component()
  .init(function ({ observer }) {
    observer('**', () => {
      // 每次 setData 都触发
    })
  })
  .register()

注意事项

请特别留意:数据监听器监听的是 setData 涉及到的数据字段 而非变化的数据字段,即使这些数据字段的值没有发生变化,数据监听器依然会被触发。想要在此基础上,过滤未变化的数据字段、有更简便的语法,可以使用 computed 扩展模块。

另外,在编写逻辑时,如果在数据监听器函数中使用 setData 设置本身监听的数据字段,可能会导致死循环,需要特别留意。

数据控制概述

操作组件数据 data 时,有一些高级手段来控制 data 自身的维护方式、提升性能:

  • 如果需要监听数据的变化,可以使用 数据监听器 ;
  • 除了 setData,还有其他一些手段来更高效地更新数据,请参考 高级数据更新方法 和 数据更新策略 ;
  • 数据字段在组件之间会深拷贝,要改变这一行为,请参考 数据字段拷贝控制 。

data 在 WXML 上的应用方式也可以微调,以提升 WXML 模版更新性能:

  • 选用合适的 组件初始化策略 可以减少一些不必要的模板更新;
  • 使用 纯数据字段 可以禁止部分 data 数据字段用于 WXML 模板。

此外,在开发者工具的 WXML 面板中可以查看自定义组件实例的数据:先选中需要查看的自定义组件,然后切换到 Component Data 即可实时查看当前自定义组件的数据。

组件树访问

通过组件树访问接口,可以直接获得另一个自定义组件实例 this。这样就可以自由地操作多个组件。

需要注意的是,滥用这个接口可能导致组件间过度耦合。对于更好的组件间通信方式,请参考 组件间交互概述 。

想要正确理解组件树的概念,请仔细阅读 组件框架模型 。

获取组件实例

两个有引用与被引用关系的组件,通常可以称为父组件和子组件。自定义组件可以获得引用它的自定义组件(父组件)实例、所有它引用的自定义组件(子组件)实例。

获取父组件实例

通过 this.selectOwnerComponent 可以获得父组件实例 this。对于页面根组件,将返回 null

export default Component({
  lifetimes: {
    attached() {
      const owner = this.selectOwnerComponent()
    },
  },
})

如果使用 Chaining 形式,可以结合 trait behaviors 来进行逻辑解耦。

// common.ts

export interface IFooTrait {
  bar(): string
}

export const fooTrait = Behavior.trait<IFooTrait>()
// 父组件可以实现 fooTrait
export default Component()
  .init(function ({ implement }) {
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })
  })
  .register()
// 子组件使用 trait behaviors 接口来访问父组件
export default Component()
  .init(function ({ lifetime }) {
    lifetime('attached', () => {
      const owner = this.selectOwnerComponent()!
      const fooImpl = owner.traitBehavior(fooTrait)!
      fooImpl.bar() // 'bar'
    })
  })
  .register()

获取子组件实例

通过 this.selectComponent 可以获得自定义组件 Shadow 树中的任意自定义组件实例 this

调用时需要传入一个选择器 selector,如:this.selectComponent(".my-component")。选择器详细语法可查看 selector 语法参考文档。

另有一个类似的接口 this.selectAllComponents。他们的区别是,前者获取第一个匹配到选择器的节点,后者返回一个数组,包含所有匹配到的节点。

export default Component({
  lifetimes: {
    attached() {
      const comp = this.selectComponent(".my-component")
    },
  },
})

在上例中,将会获取到 Shadow 树中 classmy-component 的子组件实例 this

注意:默认情况下,微信小程序与插件之间、不同插件之间的组件将无法通过组件树访问接口得到组件实例(将返回 null)。如果想让一个组件在上述条件下依然能被 selectComponent 返回,可以自定义其返回结果(见下)。

自定义的组件实例获取结果

从基础库版本 2.2.3 开始提供支持。对于 exparser 组件框架,需要引入内置 behavior wx://component-export 这一功能才会生效。

selectOwnerComponent selectComponent selectAllComponents 返回的结果,可以通过 export 定义段来改变。

// 子组件 my-component 内部
Component({
  behaviors: ['wx://component-export'],
  export() {
    return { myField: 'myValue' }
  }
})
<!-- 使用自定义组件时 -->
<my-component id="the-id" />
// 父组件调用
const child = this.selectComponent('#the-id') // 等于 { myField: 'myValue' }

在上例中,父组件获取 idthe-id 的子组件实例时,得到的是对象 { myField: 'myValue' }

trait behaviors

如果一个组件需要暴露一些 JS 接口给另一些组件使用,可以用 methods、export 等方式,但它们都有些这样那样的问题。

现在最推荐的方式是使用 trait behaviors。它可以有效解耦逻辑,并具有很好的 TypeScript 支持。

目前,trait behaviors 只有 glass-easel 组件框架原生支持。想在 exparser 组件框架上使用,可以借助 chaining-api-polyfill 。但需要注意:chaining-api-polyfill 定义的 trait behaviors 只能在由它定义的组件中使用;反之,glass-easel 原生定义 trait behaviors 也只能在 glass-easel 原生定义的组件中使用。

trait behaviors 基本用法

设计理念上,trait behaviors 主要是配合 TypeScript 类型使用的。因此,以下例子都用 TypeScript 编写。

定义 trait behaviors

首先,使用 trait behaviors 定义一组接口。这组接口可以有个 TypeScript 声明。

// 定义一组接口
export interface IFooTrait {
  bar(): string
}

// 将 TypeScript 接口定义包装成 trait behavior
export const fooTrait = Behavior.trait<IFooTrait>()

实现 trait behaviors

提供接口实现的组件,就需要实现 trait behaviors。实现时,需要使用 Chaining 形式。

export default Component()
  .init(function ({ implement }) {
    // implement 应在 init 函数返回前调用
    // 不应该写在生命周期函数、事件回调函数、其他方法内部
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })
  })
  .register()

调用 trait behaviors 实现

对于已经实现了 trait behaviors 的组件,通过这个组件的 this 可以访问到它的实现。

export default Component()
  .init(function ({ implement, lifetime }) {
    // 实现 trait behavior
    implement(fooTrait, {
      bar() {
        return 'bar'
      },
    })

    lifetime('attached', () => {
      // 获取组件实例 `this` 对 fooTrait 的实现(未实现的话,返回 undefined)
      const fooImpl = this.traitBehavior(fooTrait)!

      // 可以调用 trait behaviors 中的方法
      fooImpl.bar() // 'bar'
    })
  })
  .register()

实践中,更常见的情况是 trait behaviors 配合 组件间关系 或 组件树访问 来一同使用,请参考它们的文档。

trait behaviors 的扩展方法

trait behaviors 本身可以包含一些扩展方法。这些扩展方法可以基于已有方法扩展出一些新的接口,有时可以用来简化代码。

具体来说,Behavior.trait 可以提供一个转换函数,例如:

// 实现者需要实现的方法
interface IPlusTrait {
  plus(a: number, b: number): number
}

// 调用者可以调用的方法
interface IPlusMinusTrait extends IPlusTrait {
  minus(a: number, b: number): number
}

// Behavior.trait 可以传递一个转换函数,将实现者实现的 impl 转换为调用者可以获取的对象
export const plusMinusTrait = Behavior.trait<IPlusTrait, IPlusMinusTrait>((impl) => ({
  // 添加一些调用者可以调用的方法
  minus(a: number, b: number) {
    return impl.plus(a, -b)
  },
  ...impl
}))

转换函数也可以裁剪掉一些不暴露给调用者的接口(不完全暴露 ...impl)。

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 属性,不过一般并不常用。