小程序开发设计如何实现跨平台开发与管理满足企业多元化需求
344
2024-06-30
Component 构造器可用于定义组件,调用 Component 构造器时可以指定组件的属性、数据、方法等。
详细的参数含义和使用请参考 Component 参考文档。
Component({ behaviors: [], properties: { myProperty: { // 属性名 type: String, value: '' }, myProperty2: String // 简化的定义方式 }, data: {}, // 私有数据,可用于模板渲染 lifetimes: { // 生命周期函数,可以为函数,或一个在methods段中定义的方法名 attached: function () { }, detached: function () { }, }, // 生命周期函数,可以为函数,或一个在methods段中定义的方法名 attached: function () { }, // 此处attached的声明会被lifetimes字段中的声明覆盖 ready: function() { }, methods: { onMyButtonTap: function(){ this.setData({ // 更新属性和数据的方法与更新页面数据的方法类似 }) }, // 内部方法建议以下划线开头 _myPrivateMethod: function(){ // 这里将 data.A[0].B 设为 'myPrivateData' this.setData({ 'A[0].B': 'myPrivateData' }) }, _propertyChange: function(newVal, oldVal) { } }}) 已复制代码
事实上,小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造,拥有与普通组件一样的定义段与实例方法。但此时要求对应 json 文件中包含 usingComponents 定义段。
此时,组件的属性可以用于接收页面的参数,如访问页面 /pages/index/index?paramA=123¶mB=xyz ,如果声明有属性 paramA 或 paramB ,则它们会被赋值为 123 或 xyz 。
页面的生命周期方法(即 on 开头的方法),应写在 methods 定义段中。
代码示例
{ "usingComponents": {}} 已复制代码
Component({ properties: { paramA: Number, paramB: String, }, methods: { onLoad: function() { this.data.paramA // 页面参数 paramA 的值 this.data.paramB // 页面参数 paramB 的值 } }}) 已复制代码
使用 Component 构造器来构造页面的一个好处是可以使用 behaviors 来提取所有页面中公用的代码段。
例如,在所有页面被创建和销毁时都要执行同一段代码,就可以把这段代码提取到 behaviors 中。
代码示例
// page-common-behavior.jsmodule.exports = Behavior({ attached: function() { // 页面创建时执行 console.info('Page loaded!') }, detached: function() { // 页面销毁时执行 console.info('Page unloaded!') }})// 页面 Avar pageCommonBehavior = require('./page-common-behavior')Component({ behaviors: [pageCommonBehavior], data: { /* ... */ }, methods: { /* ... */ },})// 页面 Bvar pageCommonBehavior = require('./page-common-behavior')Component({ behaviors: [pageCommonBehavior], data: { /* ... */ }, methods: { /* ... */ },}) 已复制代码
组件间的基本通信方式有以下几种。
FXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(自基础库版本 1.0.0 开始,还可以在数据中包含函数)。
事件:用于子组件向父组件传递数据,可以传递任意数据。
如果以上两种方式不足以满足需要,父组件还可以通过 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) } }}) 已复制代码
触发事件的选项包括:
选项名 | 类型 | 是否必填 | 默认值 | 描述 |
---|---|---|---|---|
bubbles | Boolean | 否 | false | 事件是否冒泡 |
composed | Boolean | 否 | false | 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部 |
capturePhase | Boolean | 否 | false | 事件是否拥有捕获阶段 |
// 页面 page.FXML<another-component bindcustomevent="pageEventListener1"> <my-component bindcustomevent="pageEventListener2"></my-component></another-component>// 组件 another-component.FXML<view bindcustomevent="anotherEventListener"> <slot /></view>// 组件 my-component.FXML<view bindcustomevent="myEventListener"> <slot /></view> 已复制代码
// 组件 my-component.jsComponent({ methods: { onTap: function(){ this.triggerEvent('customevent', {}) // 只会触发 pageEventListener2 this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1 this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1 } }}) 已复制代码
可在父组件里调用 this.selectComponent ,获取子组件的实例对象(插件的自定义组件将返回 null)。
调用时需要传入一个匹配选择器 selector,如:this.selectComponent(".my-component")。
// 父组件Page({ data: {}, getChildComponent: function () { const child = this.selectComponent('.my-component'); console.log(child) }}) 已复制代码
在上例中,父组件将会获取 class 为 my-component 的子组件实例对象,即子组件的 this 。
使自定义组件中支持 export 定义段,这个定义段可以用于指定组件被 selectComponent 调用时的返回值。
代码示例
// 自定义组件 my-component 内部Component({ behaviors: ['wx://component-export'], export() { return { myField: 'myValue' } }}) 已复制代码
<!-- 使用自定义组件时 --><my-component id="the-id" /> 已复制代码
// 父组件调用const child = this.selectComponent('#the-id') // 等于 { myField: 'myValue' } 已复制代码
在上例中,父组件获取 id 为 the-id 的子组件实例的时候,得到的是对象 { myField: 'myValue' } 。
组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发。
其中,最重要的生命周期是 created、attached、detached ,包含一个组件实例生命流程的最主要时间点。
组件实例刚刚被创建好时, created 生命周期被触发。此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data 。 **此时还不能调用 setData **。 通常情况下,这个生命周期只应该用于给组件 this 添加一些自定义属性字段。
在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行。
在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发。
生命周期方法可以直接定义在 Component 构造器的第一级参数中。
自小程序基础库版本 1.0.2 起,组件的的生命周期也可以在 lifetimes 字段内进行声明(其优先级最高)。
代码示例
Component({ lifetimes: { attached: function() { // 在组件实例进入页面节点树时执行 }, detached: function() { // 在组件实例被从页面节点树移除时执行 }, }, // 以下是旧式的定义方式,可以保持对 <1.0.2 版本基础库的兼容 attached: function() { // 在组件实例进入页面节点树时执行 }, detached: function() { // 在组件实例被从页面节点树移除时执行 }, // ...}) 已复制代码
在 behaviors 中也可以编写生命周期方法,同时不会与其他 behaviors 中的同名生命周期相互覆盖。但要注意,如果一个组件多次直接或间接引用同一个 behavior ,这个 behavior 中的生命周期函数在一个执行时机内只会执行一次。
可用的全部生命周期如下表所示:
生命周期 | 参数 | 描述 | 最低版本 |
---|---|---|---|
created | 无 | 在组件实例刚刚被创建时执行 | 1.0.0 |
attached | 无 | 在组件实例进入页面节点树时执行 | 1.0.0 |
ready | 无 | 在组件在视图层布局完成后执行 | 1.0.0 |
detached | 无 | 在组件实例被从页面节点树移除时执行 | 1.0.0 |
error | Object Error | 每当组件方法抛出错误时执行 | 2.1.7 |
behaviors 是用于组件间代码共享的特性,类似于一些编程语言中的 “mixins” 或 “traits”。
每个 behavior 可以包含一组属性、数据、生命周期函数和方法。组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。 每个组件可以引用多个 behavior ,behavior 也可以引用其它 behavior 。
组件引用时,在 behaviors 定义段中将它们逐个列出即可。
代码示例
// my-behavior.jsmodule.exports = Behavior({ behaviors: [], properties: { myBehaviorProperty: { type: String } }, data: { myBehaviorData: 'my-behavior-data' }, created: function () { console.log('[my-behavior] created') }, attached: function () { console.log('[my-behavior] attached') }, ready: function () { console.log('[my-behavior] ready') }, methods: { myBehaviorMethod: function () { console.log('[my-behavior] log by myBehaviorMehtod') }, }}) 已复制代码
// my-component.jsvar myBehavior = require('my-behavior')Component({ behaviors: [myBehavior], properties: { myProperty: { type: String } }, data: { myData: 'my-component-data' }, created: function () { console.log('[my-component] created') }, attached: function () { console.log('[my-component] attached') }, ready: function () { console.log('[my-component] ready') }, methods: { myMethod: function () { console.log('[my-component] log by myMethod') }, }}) 已复制代码
在上例中, my-component 组件定义中加入了 my-behavior,而 my-behavior 结构为:
属性:myBehaviorProperty
数据字段:myBehaviorData
方法:myBehaviorMethod
生命周期函数:attached、created、ready
这将使 my-component 最终结构为:
属性:myBehaviorProperty、myProperty
数据字段:myBehaviorData、myData
方法:myBehaviorMethod、myMethod
生命周期函数:attached、created、ready
当组件触发生命周期时,上例生命周期函数执行顺序为:
[my-behavior] created
[my-component] created
[my-behavior] attached
[my-component] attached
[my-behavior] ready
[my-component] ready
详细规则参考 同名字段的覆盖和组合规则。
组件和它引用的 behavior 中可以包含同名的字段,对这些字段的处理方法如下:
如果有同名的属性 (properties) 或方法 (methods):
若组件本身有这个属性或方法,则组件的属性或方法会覆盖 behavior 中的同名属性或方法;
若组件本身无这个属性或方法,则在组件的 behaviors 字段中定义靠后的 behavior 的属性或方法会覆盖靠前的同名属性或方法;
在 2 的基础上,若存在嵌套引用 behavior 的情况,则规则为:父 behavior 覆盖 子 behavior 中的同名属性或方法。
如果有同名的数据字段 (data):
若同名的数据字段都是对象类型,会进行对象合并;
其余情况会进行数据覆盖,覆盖规则为:组件 > 父 behavior > 子 behavior 、 靠后的 behavior > 靠前的 behavior。(优先级高的覆盖优先级低的,最大的为优先级最高)
生命周期函数不会相互覆盖,而是在对应触发时机被逐个调用:
behavior 优先于组件执行;
子 behavior 优先于 父 behavior 执行;
靠前的 behavior 优先于 靠后的 behavior 执行;
对于不同的生命周期函数之间,遵循组件生命周期函数的执行顺序;
对于同种生命周期函数,遵循如下规则:
如果同一个 behavior 被一个组件多次引用,它定义的生命周期函数只会被执行一次。
自定义组件可以通过引用内置的 behavior 来获得内置组件的一些行为。
Component({ behaviors: ['wx://form-field']}) 已复制代码
在上例中, wx://form-field 代表一个内置 behavior ,它使得这个自定义组件有类似于表单控件的行为。
内置 behavior 往往会为组件添加一些属性。在没有特殊说明时,组件可以覆盖这些属性来改变它的 type 或添加 observer 。
使自定义组件有类似于表单控件的行为。 form 组件可以识别这些自定义组件,并在 submit 事件中返回组件的字段名及其对应字段值。
使 form 组件可以识别到这个自定义组件内部的所有表单控件。
使 form 组件可以识别到这个自定义组件内部的 button 。如果自定义组件内部有设置了 form-type 的 button ,它将被组件外的 form 接受。
使自定义组件支持 export 定义段。这个定义段可以用于指定组件被 selectComponent 调用时的返回值。
有时需要实现这样的组件:
<custom-ul> <custom-li> item 1 </custom-li> <custom-li> item 2 </custom-li></custom-ul> 已复制代码
这个例子中, custom-ul 和 custom-li 都是自定义组件,它们有相互间的关系,相互间的通信往往比较复杂。此时在组件定义时加入 relations 定义段,可以解决这样的问题。
示例代码
// path/to/custom-ul.jsComponent({ 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.jsComponent({ 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-input 和 custom-submit 两个组件。此时,如果这两个组件都有同一个behavior:
// path/to/custom-form-controls.jsmodule.exports = Behavior({ // ...}) 已复制代码
// path/to/custom-input.jsvar customFormControls = require('./custom-form-controls')Component({ behaviors: [customFormControls], relations: { './custom-form': { type: 'ancestor', // 关联的目标节点应为祖先节点 } }}) 已复制代码
// path/to/custom-submit.jsvar customFormControls = require('./custom-form-controls')Component({ behaviors: [customFormControls], relations: { './custom-form': { type: 'ancestor', // 关联的目标节点应为祖先节点 } }}) 已复制代码
则在 relations 关系定义中,可使用这个behavior来代替组件路径作为关联的目标节点:
// path/to/custom-form.jsvar customFormControls = require('./custom-form-controls')Component({ relations: { 'customFormControls': { type: 'descendant', // 关联的目标节点应为子孙节点 target: customFormControls } }}) 已复制代码
relations 定义段包含目标组件路径及其对应选项,可包含的选项见下表。
选项 | 类型 | 是否必填 | 描述 |
---|---|---|---|
type | String | 是 | 目标组件的相对关系,可选的值为 parent 、 child 、 ancestor 、 descendant |
linked | Function | 否 | 关系生命周期函数,当关系被建立在页面节点树中时触发,触发时机在组件attached生命周期之后 |
linkChanged | Function | 否 | 关系生命周期函数,当关系在页面节点树中发生改变时触发,触发时机在组件moved生命周期之后 |
unlinked | Function | 否 | 关系生命周期函数,当关系脱离页面节点树时触发,触发时机在组件detached生命周期之后 |
target | String | 否 | 如果这一项被设置,则它表示关联的目标节点所应具有的behavior,所有拥有这一behavior的组件节点都会被关联 |
数据-可以用于监听和响应任何属性和数据字段的变化。从小程序基础库版本 2.0.11 开始支持。
有时,在一些数据字段被 setData 设置时,需要执行一些操作。
例如, this.data.sum 永远是 this.data.numberA 与 this.data.numberB 的和。此时,可以使用数据-进行如下实现。
Component({ attached: function() { this.setData({ numberA: 1, numberB: 2, }) }, observers: { 'numberA, numberB': function(numberA, numberB) { // 在 numberA 或者 numberB 被设置时,执行这个函数 this.setData({ sum: numberA + numberB }) } }}) 已复制代码
数据-支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个-一次。
同时,-可以监听子数据字段,如下例所示。
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] }, }}) 已复制代码
如果需要监听所有子数据字段的变化,可以使用通配符 ** 。
Component({ observers: { 'some.field.**': function(field) { // 使用 setData 设置 this.data.some.field 本身或其下任何子数据字段时触发 // (除此以外,使用 setData 设置 this.data.some 也会触发) field === this.data.some.field }, }, attached: function() { // 这样会触发上面的 observer this.setData({ 'some.field': { /* ... */ } }) // 这样也会触发上面的 observer this.setData({ 'some.field.xxx': { /* ... */ } }) // 这样还是会触发上面的 observer this.setData({ 'some': { /* ... */ } }) }}) 已复制代码
特别地,仅使用通配符 ** 可以监听全部 setData 。
Component({ observers: { '**': function() { // 每次 setData 都触发 }, },}) 已复制代码
提示
数据-监听的是 setData 涉及到的数据字段,即使这些数据字段的值没有发生变化,数据-依然会被触发。
如果在数据-函数中使用 setData 设置本身监听的数据字段,可能会导致死循环,需要特别留意。
纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。
有些情况下,某些 data 中的字段(包括 setData 设置的字段)既不会展示在界面上,也不会传递给其他组件,仅仅在当前组件内部使用。
此时,可以指定这样的数据字段为“纯数据字段”,它们将仅仅被记录在 this.data 中,而不参与任何界面渲染过程,这样有助于提升页面更新性能。
指定“纯数据字段”的方法是在 Component 构造器的 options 定义段中指定 pureDataPattern 为一个正则表达式,字段名符合这个正则表达式的字段将成为纯数据字段。
代码示例:
Component({ options: { pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段 }, data: { a: true, // 普通数据字段 _b: true, // 纯数据字段 }, methods: { myMethod() { this.data._b // 纯数据字段可以在 this.data 中获取 this.setData({ c: true, // 普通数据字段 _d: true, // 纯数据字段 }) } }}) 已复制代码
上述组件中的纯数据字段不会被应用到 FXML 上:
<view wx:if="{{a}}"> 这行会被展示 </view><view wx:if="{{_b}}"> 这行不会被展示 </view> 已复制代码
属性也可以被指定为纯数据字段(遵循 pureDataPattern 的正则表达式)。
属性中的纯数据字段可以像普通属性一样接收外部传入的属性值,但不能将它直接用于组件自身的 FXML 中。
代码示例:
Component({ options: { pureDataPattern: /^_/ }, properties: { a: Boolean, _b: { type: Boolean, observer() { // 不要这样做!这个 observer 永远不会被触发 } }, }}) 已复制代码
注意:属性中的纯数据字段的属性 observer 永远不会触发!如果想要监听属性值变化,使用 数据- 代替。
也可以在页面或自定义组件的 json 文件中配置 pureDataPattern (这样就不需在 js 文件的 options 中再配置)。此时,其值应当写成字符串形式:
{ "pureDataPattern": "^_"} 已复制代码
数据- 可以用于监听纯数据字段(与普通数据字段一样)。这样,可以通过监听、响应纯数据字段的变化来改变界面。
下面的示例是一个将 JavaScript 时间戳转换为可读时间的自定义组件。
在开发者工具中预览效果
代码示例:
Component({ options: { pureDataPattern: /^timestamp$/ // 将 timestamp 属性指定为纯数据字段 }, properties: { timestamp: Number, }, observers: { timestamp: function () { // timestamp 被设置时,将它展示为可读时间字符串 var timeString = new Date(this.data.timestamp).toLocaleString() this.setData({ timeString: timeString }) } }}) 已复制代码
<view>{{timeString}}</view> 已复制代码
有时,自定义组件模板中的一些节点,其对应的自定义组件不是由自定义组件本身确定的,而是自定义组件的调用者确定的。这时可以把这个节点声明为“抽象节点”。
例如,我们现在来实现一个“选框组”(selectable-group)组件,它其中可以放置单选框(custom-radio)或者复选框(custom-checkbox)。这个组件的 fxml 可以这样编写:
代码示例:
<!-- selectable-group.fxml --><view wx: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-radio 和 custom-checkbox 需要包含在这个 fxml 对应 json 文件的 usingComponents 定义段中。
{ "usingComponents": { "custom-radio": "path/to/custom/radio", "custom-checkbox": "path/to/custom/checkbox" }} 已复制代码
抽象节点可以指定一个默认组件,当具体组件未被指定时,将创建默认组件的实例。默认组件可以在 componentGenerics 字段中指定:
{ "componentGenerics": { "selectable": { "default": "path/to/default/component" } }} 已复制代码
节点的 generic 引用 generic:xxx="yyy" 中,值 yyy 只能是静态值,不能包含数据绑定。因而抽象节点特性并不适用于动态决定节点名的场景。
为了更好定制自定义组件的功能,可以使用自定义组件扩展机制
为了更好的理解扩展后的效果,先举一个例子:
// behavior.jsmodule.exports = Behavior({ definitionFilter(defFields) { defFields.data.from = 'behavior' },})// component.jsComponent({ 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.jsmodule.exports = Behavior({ definitionFilter(defFields, definitionFilterArr) {},})// behavior2.jsmodule.exports = Behavior({ behaviors: [require('behavior3.js')], definitionFilter(defFields, definitionFilterArr) { // definitionFilterArr[0](defFields) },})// behavior1.jsmodule.exports = Behavior({ behaviors: [require('behavior2.js')], definitionFilter(defFields, definitionFilterArr) {},})// component.jsComponent({ behaviors: [require('behavior1.js')],}) 已复制代码
上述代码中声明了1个自定义组件和3个 behavior,每个 behavior 都使用了 definitionFilter 定义段。那么按照声明的顺序会有如下事情发生:
简单概括,definitionFilter 函数可以理解为当 A 使用了 B 时,A 声明就会调用 B 的 definitionFilter 函数并传入 A 的定义对象让 B 去过滤。此时如果 B 还使用了 C 和 D ,那么 B 可以自行决定要不要调用 C 和 D 的 definitionFilter 函数去过滤 A 的定义对象。
下面利用扩展简单实现自定义组件的计算属性功能:
// behavior.jsmodule.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 中,以达到计算属性的效果。
如果想要知道 setData 引发界面更新的开销,可以使用更新性能统计信息接口。它将返回每次更新中主要更新步骤发生的时间戳,可以用来大体上估计自定义组件(或页面)更新性能。例如:
Component({ attached() { // 调用时机不能早于 attached this.setUpdatePerformanceListener({withDataPaths: true}, (res) => { console.log(res) }) }}) 已复制代码
setUpdatePerformanceListener 方法接受一个 options 对象和回调函数 listener 作为参数。
其中, options 对象包含以下字段:
字段 | 类型 | 说明 |
---|---|---|
withDataPaths | Boolean | 是否返回变更的 data 字段信息 |
listeners 返回携带一个 res 对象,表示一次由 setData 引发的 更新过程 。根据 setData 调用时机的不同,更新过程大体可以分为三类:
每次成功的 setData 调用都会产生一个更新过程,使得 listener 回调一次。不过 setData 究竟触发了哪类更新过程很难判断,更新性能好坏与其具体是哪类更新也没有必然联系,只是它们的返回值参数有所不同。
res 中包含以下字段:
字段 | 类型 | 说明 |
---|---|---|
updateProcessId | Number | 此次更新过程的 ID |
parentUpdateProcessId | Number | 对于子更新,返回它所属的更新过程 ID |
isMergedUpdate | Boolean | 是否是被合并更新,如果是,则 updateProcessId 表示被合并到的更新过程 ID |
dataPaths | Array | 此次更新的 data 字段信息,只有 withDataPaths 设为 true 时才会返回 |
pendingStartTimestamp | Number | 此次更新进入等待队列时的时间戳 |
updateStartTimestamp | Number | 更新运算开始时的时间戳 |
updateEndTimestamp | Number | 更新运算结束时的时间戳 |
说明:
setUpdatePerformanceListener 只会激活当前组件或页面的统计, parentUpdateProcessId 有可能是其他组件或者页面的更新过程 ID 而未被统计回调,如果想要知道页面内所有的更新过程,需要在所有组件中都调用 setUpdatePerformanceListener ;
统计本身有一点点开销,如果想要禁用统计,调用 setUpdatePerformanceListener 时传入第二个参数 listener 为 null 即可。
基础库 3.0.38 及以上版本支持,3.0.38 以下和未配置的效果相同
在使用如 用时注入 等特性时,自定义组件所引用的其他自定义组件,在刚开始进行渲染时可能处于不可用的状态。此时,为了使渲染过程不被阻塞,不可用的自定义组件需要一个 「占位组件」(Component placeholder)。基础库会用占位组件替代不可用组件进行渲染,在该组件可用后再将占位组件替换回该组件。
一个自定义组件的占位组件可以是另一个自定义组件、或一个内置组件。
页面或自定义组件对应的 JSON 配置中的 componentPlaceholder
字段用于指定占位组件,如:
{ "usingComponents": { "comp-a": "../comp/compA", "comp-b": "../comp/compB", "comp-c": "../comp/compC" }, "componentPlaceholder": { "comp-a": "view", "comp-b": "comp-c" }} 已复制代码
该配置表示:
组件 comp-a
的占位组件为内置组件 view
组件 comp-b
的占位组件为自定义组件 comp-c
(其路径在 usingComponents
中配置)
假设该配置对应的模板如下:
<button ontap="onTap">显示组件</button><comp-a wx-if="{{ visible }}"> <comp-b prop="{{ p }}">text in slot</comp-b></comp-a> 已复制代码
小程序启动时 visible
为 false
,那么只有 button
会被渲染;点击按钮后,this.setData({ visible: true })
被执行,此时如果 comp-a
, comp-b
均不可用,则页面将被渲染为:
<button>显示组件</button><view> <comp-c prop="{{ p }}">text in slot</comp-c></view> 已复制代码
comp-a
与 comp-b
准备完成后,页面被替换为:
<button>显示组件</button><comp-a> <comp-b prop="{{ p }}">text in slot</comp-b></comp-a> 已复制代码
当一个组件被指定为占位组件时(如上例中的 comp-c
),为其指定占位组件是无效的。可以理解为如果一个组件需要作为其他组件的占位组件,则它必须在一开始就是可用的;
目前自定义组件不可用的情况包括:
使用分包异步化特性的情况下,引用了其他分包的组件,而对应分包还未-;
使用用时注入特性的情况下,该组件还未注入;
如果一个组件不可用,且其占位组件不存在,则渲染时会报错并抛出;
如果一个组件不存在,但为其指定了可用的占位组件,则占位组件可以被正常渲染,但后续尝试准备替换时会报错并抛出。
基础库尝试渲染一个组件时,会首先递归检查 usingComponents
,收集其将使用到的所有组件的信息;在这个过程中,如果某个被使用到的组件不可用,基础库会先检查其是否有对应的占位组件。如果没有,基础库会中断渲染并抛出错误;如果有,则会标记并在后续渲染流程中使用占位组件替换该不可用的组件进行渲染。不可用的组件会在当前渲染流程结束后尝试准备(-分包或注入代码等);等到准备过程完成后,再尝试渲染该组件(实际上也是在执行这个流程),并替换掉之前渲染的占位组件。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~