组件模版和样式

优质
小牛编辑
134浏览
2023-12-01

组件模版和样式

自定义组件拥有自己的 jxml 模版和 jxss 样式。

组件模版

组件模版的写法与页面模板相同。组件模版与组件数据结合后生成的节点树,将被插入到组件的引用位置上。

在组件模板中可以提供一个 <slot> 节点,用于承载组件引用时提供的子节点。

代码示例:

<!-- 组件模板 -->
<view class="box">
  <view>组件的内部节点</view>
  <slot></slot>
</view>
<!-- 引用组件的页面模版 -->
<view>
  <component-tag-name>
    <!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
    <view>这里是插入到组件slot中的内容</view>
  </component-tag-name>
</view>

注意,在模版中引用到的自定义组件及其对应的节点名需要在 json 文件中显式定义,否则会被当作一个无意义的节点。除此以外,节点名也可以被声明为抽象节点。

模版数据绑定

写法、规则、作用同页面的 JXML 模版一致。

代码示例:

<!-- 引用组件的页面模版 -->
<view>
  <component-tag-name prop-a="{{a}}" prop-b="{{b}}">
    <!-- 这部分内容将被放置在组件 <slot> 的位置上 -->
    <view>这里是插入到组件slot中的内容</view>
  </component-tag-name>
</view>

在以上例子中,组件的属性 propApropB 将收到页面传递的数据。页面可以通过 setData 来改变绑定的数据字段。

注意:这样的数据绑定只能传递 JSON

组件 jxml 的 slot

在组件的 jxml 中可以包含 slot 节点,用于承载组件使用者提供的 jxml 结构。

默认情况下,一个组件的 jxml 中只能有一个 slot。需要使用多 slot 时,可以在组件 js 中声明启用。

Component({
  options: {
    multipleSlots: true // 在组件定义时的选项中启用多slot支持
  },
  properties: { /* ... */ },
  methods: { /* ... */ }
})

组件的 jxml 中的多个 slot,以不同的 name 来区分。

<!-- 组件模板 -->
<view class="wrapper">
  <slot name="c1"></slot>
  <view>演示</view>
  <slot name="c2"></slot>
</view>

使用时,用 slot 属性来将节点插入到不同的 slot 上。

<!-- 引用组件的页面模版 -->
<view>
  <component-tag-name>
    <!-- 这部分内容将被放置在组件 <slot name="c1"> 的位置上 -->
    <view slot="c1">这里是插入到组件slot name="c1"中的内容</view>
    <!-- 这部分内容将被放置在组件 <slot name="c2"> 的位置上 -->
    <view slot="c2">这里是插入到组件slot name="c2"中的内容</view>
  </component-tag-name>
</view>

组件样式

组件对应 jxss 文件的样式,只对组件 jxml 内的节点生效。编写组件样式时,需要注意以下几点:

  • 组件和引用组件的页面不能使用 id 选择器(#a)、属性选择器([a])和标签名选择器,请改用 class 选择器。
  • 组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。
  • 子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。
  • 继承样式,如 font 、 color ,会从组件外继承到组件内。
  • 除继承样式外, app.jxss 中的样式、组件所在页面的的样式对自定义组件无效。
#a { } /* 在组件中不能使用 */
[a] { } /* 在组件中不能使用 */
button { } /* 在组件中不能使用 */
.a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */

除此以外,组件可以指定它所在节点的默认样式,使用 :host 选择器。

代码示例:

/* 组件 custom-component.jxss */
:host {
  color: #000;
}
<!-- 页面的 JXML -->
<custom-component>黑色文本</custom-component>

外部样式类

有时,组件希望接受外部传入的样式类(类似于 view 组件的 hover-class 属性)。此时可以在 Component 中用 externalClasses 定义段定义若干个外部样式类。

注意:在同一个节点上使用普通样式类和外部样式类时,两个类的优先级是未定义的,因此最好避免这种情况。

代码示例:

/* 组件 custom-component.js */
Component({
  externalClasses: ['out-class']
})
<!-- 组件 custom-component.jxml -->
<custom-component class="out-class">这段文本的颜色由组件外的 class 决定 </custom-component>

这样,组件的使用者可以指定这个样式类对应的 class ,就像使用普通属性一样。

代码示例:

<!-- 页面的 JXML -->
<custom-component out-class="blue" />
.blue {
  color: blue;
}

全局样式类

使用外部样式类可以让组件使用指定的组件外样式类,如果希望组件外样式类能够完全影响组件内部,可以将组件构造器中的options.addGlobalClass 字段置为 true。

当开放了全局样式类,存在外部样式污染组件样式的风险,请谨慎选择。

代码示例:

/* 组件 custom-component.js */
Component({
  options: {
    addGlobalClass: true,
  }
})
<!-- 组件 custom-component.jxml -->
<text class="blue">这段文本的颜色由组件外的 class 决定</text>
/* 组件外的样式定义 */
.blue {
  color: blue;
}

Component构造器

定义段与示例方法

Component构造器可用于定义组件,调用Component构造器时可以指定组件的属性、数据、方法等。

定义段类型是否必填描述
propertiesObject Map组件的对外属性,是属性名到属性设置的映射表,属性设置中可包含三个字段, type 表示属性类型、 value 表示属性初始值、 observer 表示属性值被更改时的响应函数
dataObject组件的内部数据,和 properties 一同用于组件的模版渲染
methodsObject组件的方法,包括事件响应函数和任意的自定义方法,关于事件响应函数的使用,参见 组件事件
behaviorsString Array类似于 mixins 和 traits 的组件间代码复用机制,参见 behaviors
createdFunction组件生命周期函数,在组件实例进入页面节点树时执行,注意此时不能调用 setData
attachedFunction组件生命周期函数,在组件实例进入页面节点树时执行
readyFunction组件生命周期函数,在组件布局完成后执行,此时可以获取节点信息(使用 SelectorQuery )
movedFunction组件生命周期函数,在组件实例被移动到节点树另一个位置时执行
detachedFunction组件生命周期函数,在组件实例被从页面节点树移除时执行
relationsObject组件间关系定义,参见 组件间关系
externalClassesString Array组件接受的外部样式类,参见 外部样式类
optionsObject Map一些组件选项,请参见文档其他部分的说明
lifetimesObject组件生命周期声明对象,组件的生命周期:created、attached、ready、moved、detached将收归到lifetimes字段内进行声明,原有声明方式仍旧有效,如同时存在两种声明方式,则 lifetimes 字段内声明方式优先级最高
pageLifetimesObject组件所在页面的生命周期声明对象,目前仅支持页面的 show 和 hide 两个生命周期
definitionFilterFunction定义段过滤器,用于自定义组件扩展,参见 自定义组件扩展

生成的组件实例可以在组件的方法、生命周期函数和属性 observer 中通过 this 访问。组件包含一些通用属性和方法。

属性名类型描述
isString组件的文件路径
idString节点id
datasetString节点 dataset
dataObject组件数据,包括内部数据和属性值
propertiesObject组件数据,包括内部数据和属性值(与 data 一致)
方法名参数描述
setDataObject newData设置data并执行视图层渲染
hasBehaviorObject behavior检查组件是否具有 behavior (检查时会递归检查被直接或间接引入的所有behavior)
triggerEventString name, Object detail, Object options触发事件,参见 组件事件
createSelectorQuery创建一个 SelectorQuery 对象,选择器选取范围为这个组件实例内
selectComponentString selector使用选择器选择组件实例节点,返回匹配到的第一个组件实例对象(会被 jd://component-export 影响)
selectAllComponentsString selector使用选择器选择组件实例节点,返回匹配到的全部组件实例对象组成的数组
getRelationNodesString relationKey获取所有这个关系对应的所有关联节点,参见 组件间关系

代码示例:

Component({

  behaviors: [],

  properties: {
    myProperty: { // 属性名
      type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型)
      value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个
      observer: function(newVal, oldVal, changedPath) {
         // 属性被改变时执行的函数(可选),也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange'
         // 通常 newVal 就是新设置的数据, oldVal 是旧数据
      }
    },
    myProperty2: String // 简化的定义方式
  },
  data: {}, // 私有数据,可用于模版渲染

  lifetimes: {
    // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
    attached: function () { },
    moved: function () { },
    detached: function () { },
  },

  // 生命周期函数,可以为函数,或一个在methods段中定义的方法名
  attached: function () { }, // 此处attached的声明会被lifetimes字段中的声明覆盖
  ready: function() { },

  pageLifetimes: {
    // 组件所在页面的生命周期函数
    show: function () { },
  },

  methods: {
    onMyButtonTap: function(){
      this.setData({
        // 更新属性和数据的方法与更新页面数据的方法类似
      })
    },
    // 内部方法建议以下划线开头
    _myPrivateMethod: function(){
      // 这里将 data.B[0].C 设为 'privateData'
      this.setData({
        'B[0].C': 'privateData'
      })
    },
    _propertyChange: function(newVal, oldVal) {

    }
  }

})

注意:在 properties 定义段中,属性名采用驼峰写法(propertyName);在 jxml 中,指定属性值时则对应使用连字符写法(component-tag-name property-name="attr value"),应用于数据绑定时采用驼峰写法(attr="")。

使用 Component 构造器构造页面

事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含 usingComponents 定义段。

此时,组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123&paramB=xyz ,如果声明有属性 paramA 或 paramB ,则它们会被赋值为 123 或 xyz 。

代码示例:

{
  "usingComponents": {}
}
Component({

  properties: {
    paramA: Number,
    paramB: String,
  },

  methods: {
    onLoad: function() {
      this.data.paramA // 页面参数 paramA 的值
      this.data.paramB // 页面参数 paramB 的值
    }
  }

})
Bug & Tips:
  • 使用 this.data 可以获取内部数据和属性值,但不要直接修改它们,应使用 setData 修改。
  • 生命周期函数无法在组件方法中通过 this 访问到。
  • 属性名应避免以 data 开头,即不要命名成 dataXyz 这样的形式,因为在 JXML 中, data-xyz="" 会被作为节点 dataset 来处理,而不是组件属性。
  • 在一个组件的定义和使用时,组件的属性名和 data 字段相互间都不能冲突(尽管它们位于不同的定义段中)。
  • bug : 对于 type 为 Object 或 Array 的属性,如果通过该组件自身的 this.setData 来改变属性值的一个子字段,则依旧会触发属性 observer ,且 observer 接收到的 newVal 是变化的那个子字段的值, oldVal 为空, changedPath 包含子字段的字段名相关信息。

组件间通信与事件

组件间通信

组件间的基本通信方式有以下几种。

  • JXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据
  • 事件:用于子组件向父组件传递数据,可以传递任意数据。
  • 如果以上两种方式不足以满足需要,父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。

监听事件

事件系统是组件间通信的主要方式之一。自定义组件可以触发任意的事件,引用组件的页面可以监听这些事件。关于事件的基本概念和用法,参见 事件

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

代码示例:

<!-- 当自定义组件触发“myevent”事件时,调用“onMyEvent”方法 -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- 或者可以写成 -->
<component-tag-name bind:myevent="onMyEvent" />
Page({
  onMyEvent: function(e){
    e.detail // 自定义组件触发事件时提供的detail对象
  }
})

触发事件

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

代码示例:

<!-- 在自定义组件中 -->
<button bindtap="onTap">点击这个按钮将触发“myevent”事件</button>
Component({
  properties: {}
  methods: {
    onTap: function(){
      var myEventDetail = {} // detail对象,提供给事件监听函数
      var myEventOption = {} // 触发事件的选项
      this.triggerEvent('myevent', myEventDetail, myEventOption)
    }
  }
})

触发事件的选项包括:

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

关于冒泡和捕获阶段的概念,请阅读事件章节中的相关说明。

代码示例:

// 页面 page.jxml
<another-component bindcustomevent="pageEventListener1">
  <my-component bindcustomevent="pageEventListener2"></my-component>
</another-component>
// 组件 another-component.jxml
<view bindcustomevent="anotherEventListener">
  <slot />
</view>
// 组件 my-component.jxml
<view bindcustomevent="myEventListener">
  <slot />
</view>
// 组件 my-component.js
Component({
  methods: {
    onTap: function(){
      this.triggerEvent('customevent', {}) // 只会触发 pageEventListener2
      this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1
      this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1
    }
  }
})

behaviors

定义和使用 behaviors

behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的“mixins”或“traits”。

每个 behavior 可以包含一组属性、数据、生命周期函数和方法,组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。每个组件可以引用多个 behaviorbehavior 也可以引用其他 behavior

behavior 需要使用 Behavior() 构造器定义。

代码示例:

// my-behavior.js
module.exports = Behavior({
  behaviors: [],
  properties: {
    myBehaviorProperty: {
      type: String
    }
  },
  data: {
    myBehaviorData: {}
  },
  attached: function(){},
  methods: {
    myBehaviorMethod: function(){}
  }
})

组件引用时,在 behaviors 定义段中将它们逐个列出即可。

代码示例:

// my-component.js
var myBehavior = require('my-behavior')
Component({
  behaviors: [myBehavior],
  properties: {
    myProperty: {
      type: String
    }
  },
  data: {
    myData: {}
  },
  attached: function(){},
  methods: {
    myMethod: function(){}
  }
})

在上例中, my-component 组件定义中加入了my-behavior ,而my-behavior 中包含有 myBehaviorProperty 属性、 myBehaviorData 数据字段、 myBehaviorMethod 方法和一个 attached 生命周期函数。这将使得 my-component 中最终包含 myBehaviorPropertymyProperty 两个属性, myBehaviorDatamyData 两个数据字段,和 myBehaviorMethodmyMethod 两个方法。当组件触发 attached 生命周期时,会依次触发my-behavior 中的 attached 生命周期函数和 my-component 中的 attached 生命周期函数。

字段的覆盖和组合规则

组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:

  • 如果有同名的属性或方法,组件本身的属性或方法会覆盖 behavior 中的属性或方法,如果引用了多个 behavior ,在定义段中靠后 behavior 中的属性或方法会覆盖靠前的属性或方法;
  • 如果有同名的数据字段,如果数据是对象类型,会进行对象合并,如果是非对象类型则会进行相互覆盖;
  • 生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用。如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。

内置 behaviors

自定义组件可以通过引用内置的 behavior 来获得内置组件的一些行为。

代码示例:

Component({
  behaviors: ['jd://form-field']
})

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

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

jd://form-field

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

属性名类型描述
nameString在表单中的字段名
value任意在表单中的字段值

jd://component-export

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

未使用这个定义段时, selectComponent 将返回自定义组件的 this (插件的自定义组件将返回 null )。使用这个定义段时,将以这个定义段的函数返回值代替。

代码示例:

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

组件间关系

定义和使用组件间关系

有时需要实现这样的组件:

<custom-ul>
  <custom-li> item 1 </custom-li>
  <custom-li> item 2 </custom-li>
</custom-ul>

这个例子中, custom-ulcustom-li 都是自定义组件,它们有相互间的关系,相互间的通信往往比较复杂。此时在组件定义时加入 relations 定义段,可以解决这样的问题。示例:

// path/to/custom-ul.js
Component({
  relations: {
    './custom-li': {
      type: 'child', // 关联的目标节点应为子节点
      linked: function(target) {
        // 每次有custom-li被插入时执行,target是该节点实例对象,触发在该节点attached生命周期之后
      },
      linkChanged: function(target) {
        // 每次有custom-li被移动后执行,target是该节点实例对象,触发在该节点moved生命周期之后
      },
      unlinked: function(target) {
        // 每次有custom-li被移除时执行,target是该节点实例对象,触发在该节点detached生命周期之后
      }
    }
  },
  methods: {
    _getAllLi: function(){
      // 使用getRelationNodes可以获得nodes数组,包含所有已关联的custom-li,且是有序的
      var nodes = this.getRelationNodes('path/to/custom-li')
    }
  },
  ready: function(){
    this._getAllLi()
  }
})
// path/to/custom-li.js
Component({
  relations: {
    './custom-ul': {
      type: 'parent', // 关联的目标节点应为父节点
      linked: function(target) {
        // 每次被插入到custom-ul时执行,target是custom-ul节点实例对象,触发在attached生命周期之后
      },
      linkChanged: function(target) {
        // 每次被移动后执行,target是custom-ul节点实例对象,触发在moved生命周期之后
      },
      unlinked: function(target) {
        // 每次被移除时执行,target是custom-ul节点实例对象,触发在detached生命周期之后
      }
    }
  }
})

注意:必须在两个组件定义中都加入 relations 定义,否则不会生效。

关联一类组件

有时,需要关联的是一类组件,如:

<custom-form>
  <view>
    input
    <custom-input></custom-input>
  </view>
  <custom-submit> submit </custom-submit>
</custom-form>

custom-form 组件想要关联 custom-inputcustom-submit 两个组件。此时,如果这两个组件都有同一个 behavior:

// path/to/custom-form-controls.js
module.exports = Behavior({
  // ...
})
// path/to/custom-input.js
var customFormControls = require('./custom-form-controls')
Component({
  behaviors: [customFormControls],
  relations: {
    './custom-form': {
      type: 'ancestor', // 关联的目标节点应为祖先节点
    }
  }
})
// path/to/custom-submit.js
var customFormControls = require('./custom-form-controls')
Component({
  behaviors: [customFormControls],
  relations: {
    './custom-form': {
      type: 'ancestor', // 关联的目标节点应为祖先节点
    }
  }
})

则在 relations 关系定义中,可使用这个 behavior 来代替组件路径作为关联的目标节点:

// path/to/custom-form.js
var customFormControls = require('./custom-form-controls')
Component({
  relations: {
    'customFormControls': {
      type: 'descendant', // 关联的目标节点应为子孙节点
      target: customFormControls
    }
  }
})

relations 定义段

relations 定义段包含目标组件路径及其对应选项,可包含的选项见下表。

选项类型是否必填描述
typeString目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant
linkedFunction关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件 attached 生命周期之后
linkChangedFunction关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件 moved 生命周期之后
unlinkedFunction关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件 detached 生命周期之后
targetString如果这一项被设置,则它表示关联的目标节点所应具有的 behavior,所有拥有这一 behavior 的组件节点都会被关联

抽象节点

在组件中使用抽象节点

有时,自定义组件模版中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。

例如,我们现在来实现一个“选框组”(selectable-group)组件,它其中可以放置单选框(custom-radio)或者复选框(custom-checkbox)。这个组件的 jxml 可以这样编写:

<!-- selectable-group.jxml -->
<view jd:for="{{labels}}">
  <label>
    <selectable disabled="{{false}}"></selectable>
    {{item}}
  </label>
</view>

其中,“selectable” 不是任何在 json 文件的 usingComponents 字段中声明的组件,而是一个抽象节点。它需要在 componentGenerics 字段中声明:

{
  "componentGenerics": {
    "selectable": true
  }
}

使用包含抽象节点的组件

在使用 selectable-group 组件时,必须指定“selectable”具体是哪个组件:

<selectable-group generic:selectable="custom-radio" />

这样,在生成这个 selectable-group 组件的实例时,“selectable”节点会生成 “custom-radio” 组件实例。类似地,如果这样使用:

<selectable-group generic:selectable="custom-checkbox" />

“selectable”节点则会生成 “custom-checkbox” 组件实例。

注意:上述的 custom-radiocustom-checkbox 需要包含在这个 jxml 对应 json 文件的 usingComponents 定义段中。

{
  "usingComponents": {
    "custom-radio": "path/to/custom/radio",
    "custom-checkbox": "path/to/custom/checkbox"
  }
}

抽象节点的默认组件

抽象节点可以指定一个默认组件,当具体组件未被指定时,将创建默认组件的实例。默认组件可以在 componentGenerics 字段中指定:

{
  "componentGenerics": {
    "selectable": {
      "default": "path/to/default/component"
    }
  }
}
Tips:
  • 节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。

自定义组件扩展

为了更好定制自定义组件的功能,可以使用自定义组件扩展机制。

扩展后的效果

为了更好的理解扩展后的效果,先举一个例子:

// behavior.js
module.exports = Behavior({
  definitionFilter(defFields) {
    defFields.data.from = 'behavior'
  },
})
// component.js
Component({
  data: {
    from: 'component'
  },
  behaviors: [require('behavior.js')],
  ready() {
    console.log(this.data.from) // 此处会发现输出 behavior 而不是 component
  }
})

通过例子可以发现,自定义组件的扩展其实就是提供了修改自定义组件定义段的能力,上述例子就是修改了自定义组件中的 data 定义段里的内容。

使用扩展

Behavior() 构造器提供了新的定义段 definitionFilter ,用于支持自定义组件扩展。 definitionFilter 是一个函数,在被调用时会注入两个参数,第一个参数是使用该 behavior 的 component/behavior 的定义对象,第二个参数是该 behavior 所使用的 behavior 的 definitionFilter 函数列表。

以下举个例子来说明:

// behavior3.js
module.exports = Behavior({
    definitionFilter(defFields, definitionFilterArr) {},
})
// behavior2.js
module.exports = Behavior({
  behaviors: [require('behavior3.js')],
  definitionFilter(defFields, definitionFilterArr) {
    // definitionFilterArr[0](defFields)
  },
})
// behavior1.js
module.exports = Behavior({
  behaviors: [require('behavior2.js')],
  definitionFilter(defFields, definitionFilterArr) {},
})
// component.js
Component({
  behaviors: [require('behavior1.js')],
})

上述代码中声明了 1 个自定义组件和 3 个 behavior,每个 behavior 都使用了 definitionFilter 定义段。那么按照声明的顺序会有如下事情发生:

  1. 当进行 behavior2 的声明时就会调用 behavior3 的 definitionFilter 函数,其中 defFields 参数是 behavior2 的定义段, definitionFilterArr 参数即为空数组,因为 behavior3 没有使用其他的 behavior 。

  2. 当进行 behavior1 的声明时就会调用 behavior2 的 definitionFilter 函数,其中 defFields 参数是 behavior1 的定义段, definitionFilterArr 参数是一个长度为1的数组,definitionFilterArr[0] 即为 behavior3 的 definitionFilter 函数,因为 behavior2 使用了 behavior3。用户在此处可以自行决定在进行 behavior1 的声明时要不要调用 behavior3 的 definitionFilter 函数,如果需要调用,在此处补充代码 definitionFilterArr[0](defFields) 即可,definitionFilterArr 参数会由基础库补充传入。

  3. 同理,在进行 component 的声明时就会调用 behavior1 的 definitionFilter 函数。

简单概括,definitionFilter 函数可以理解为当 A 使用了 B 时,A 声明就会调用 B 的 definitionFilter 函数并传入 A 的定义对象让 B 去过滤。此时如果 B 还使用了 C 和 D ,那么 B 可以自行决定要不要调用 C 和 D 的 definitionFilter 函数去过滤 A 的定义对象。

代码示例:

真实案例

下面利用扩展简单实现自定义组件的计算属性功能:

// behavior.js
module.exports = Behavior({
  lifetimes: {
    created() {
      this._originalSetData = this.setData // 原始 setData
      this.setData = this._setData // 封装后的 setData
    }
  },
  definitionFilter(defFields) {
    const computed = defFields.computed || {}
    const computedKeys = Object.keys(computed)
    const computedCache = {}

    // 计算 computed
    const calcComputed = (scope, insertToData) => {
      const needUpdate = {}
      const data = defFields.data = defFields.data || {}

      for (let key of computedKeys) {
        const value = computed[key].call(scope) // 计算新值
        if (computedCache[key] !== value) needUpdate[key] = computedCache[key] = value
        if (insertToData) data[key] = needUpdate[key] // 直接插入到 data 中,初始化时才需要的操作
      }

      return needUpdate
    }    

    // 重写 setData 方法
    defFields.methods = defFields.methods || {}
    defFields.methods._setData = function (data, callback) {
      const originalSetData = this._originalSetData // 原始 setData
      originalSetData.call(this, data, callback) // 做 data 的 setData
      const needUpdate = calcComputed(this) // 计算 computed
      originalSetData.call(this, needUpdate) // 做 computed 的 setData
    }

    // 初始化 computed
    calcComputed(defFields, true) // 计算 computed
  }
})

在组件中使用:

const beh = require('./behavior.js')
Component({
  behaviors: [beh],
  data: {
    a: 0,
  },
  computed: {
    b() {
      return this.data.a + 100
    },
  },
  methods: {
    onTap() {
      this.setData({
        a: ++this.data.a,
      })
    }
  }
})
<view>data: {{a}}</view>
<view>computed: {{b}}</view>
<button bindtap="onTap">click</button>

实现原理很简单,对已有的 setData 进行二次封装,在每次 setData 的时候计算出 computed 里各字段的值,然后设到 data 中,已达到计算属性的效果。

此实现只是作为一个简单案例来展示,请勿直接在生产环境中使用。