Custom Component Extension

To better customize the functionality of custom components, you can use the custom component extension mechanism. This mechanism is supported from Mini Program base library 2.2.3 and later.

Effect of Extension

In order to better understand the effect of the extension, look at the example below:

Preview with Developer Tool

// 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) // You can see that the output here is behavior instead of component
  }
})

As you can see from the example, extensions to custom components provide the ability to modify the data definition field in custom component.

Using Extensions

The Behavior() constructor provides a new definition field, definitionFilter to support the custom component extension. The definitionFilter function is injected with the following two parameters when it is called: the component/behavior definition object that uses this behavior, and the definitionFilter function list of the behavior used by this behavior.

Here is an example:

// 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')],
})

The code above declares 1 custom component and 3 behaviors. Each behavior uses a definitionFilter definition field. Then, the following events occur in the order of the declaration:

  1. When the behavior2 declaration is made, the definitionFilter function of behavior3 is called. The defFields parameter is the definition field of behavior2, and the definitionFilterArr parameter is an empty array because behavior3 does not use any other behavior.
  2. When the behavior1 declaration is made, the definitionFilter function of behavior2 is called. The defFields parameter is the definition field of behavior1, the definitionFilterArr parameter is an array with a length of 1, and definitionFilterArr[0] is the definitionFilter function of behavior3 because behavior3 is used by behavior2. You can decide whether to call the definitionFilter function of behavior3 when the behavior1 declaration is made. If the call is needed, add the code definitionFilterArr[0](defFields) here and the definitionFilterArr parameter will be passed by the base library.
  3. Similarly, when component is declared, the definitionFilter function of behavior1 is called.

To put it simply, the definitionFilter function can be interpreted as the fact that when A uses B, the A declaration calls the definitionFilter function of B and passes the definition object of A for B to filter. If B also uses C and D, it can decide whether to call the definitionFilter function of C and D to filter the defined object of A.

Code example:

Preview with Developer Tool

Use Case

The following is a simple implementation of the computed property for a custom component with an extension:

// behavior.js
module.exports = Behavior({
  lifetimes: {
    created() {
      this._originalSetData = this.setData // Original setData
      this.setData = this._setData // Packaged setData
    }
  },
  definitionFilter(defFields) {
    const computed = defFields.computed || {}
    const computedKeys = Object.keys(computed)
    const computedCache = {}

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

      for (let key of computedKeys) {
        const value = computed[key].call(scope) // Calculate new value
        if (computedCache[key] !== value) needUpdate[key] = computedCache[key] = value
        if (insertToData) data[key] = needUpdate[key] // Insert the value directly into the data, which is only needed during initialization
      }

      return needUpdate
    }

    // Override the setData method
    defFields.methods = defFields.methods || {}
    defFields.methods._setData = function (data, callback) {
      const originalSetData = this._originalSetData // Original setData
      originalSetData.call(this, data, callback) // Perform setData for data
      const needUpdate = calcComputed(this) // Calculate computed
      originalSetData.call(this, needUpdate) // Perform setData for computed
    }

    // Initialize computed
    calcComputed(defFields, true) // Calculate computed
  }
})

Used in components:

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>

The implementation principle is simple: Perform a secondary encapsulation for an existing setData, calculate the value of each field in computed each time you perform setData, and then set it into the data to implement computed property.

This example is for reference only. Do not use it directly for production.

Official extension package