Skip to content

自定義組件

1. 介紹

開發者可以將頁面內的功能模組抽象成自定義組件,以便在不同的頁面中重複使用; 也可以將複雜的頁面折開成多個低耦合的模塊,有助於代碼維護自定義組件在使用時與基礎組件非常相似。

1.1 創建自定義組件

類似於頁面,一個自定義組件由json wxml wxss js4個文件組成要編寫一個自定義組件,首先需要在json文件中進行自定義組件聲明(將component欄位設為true可將這一組文件設為自定義組件):

js
{
  "component": true
}

同時,還要在wxml文件中編寫組件模版,在wxss文件中加入組件樣式,它們的寫法與頁面的寫法類似具體細節和注意事項參見 組件模版和樣式

代碼示例:

js
<!-- This is the internal WXML structure of the custom component -->
<view class="inner">
  {{innerText}}
</view>
<slot></slot>
js
/*  This style only applies to this custom component  */
.inner {
  color: red;
}

TIP

在組件wxss中不應使用ID選擇器、内容選擇器和標籤名選擇器。

在自定義組件的js文件中,需要使用Component()來注册組件,並提供組件的内容定義、內部數據和自定義方法

組件的屬性值和內部數據將被用於組件wxml的渲染,其中,屬性值是可由組件外部傳入的更多細節參見 Component構造器

代碼示例:

js
Component({
  properties: {
    // The innerText property is defined here, and the property value can be specified when the component is used.
    innerText: {
      type: String,
      value: "default value",
    },
  },
  data: {
    // Here are some component internal data
    someData: {},
  },
  methods: {
    // Here is a custom method
    customMethod: function () {},
  },
});

1.2 使用自定義組件

使用已注册的自定義組件前,首先要在頁面的json文件中進行引用聲明此時需要提供每個自定義組件的標籤名和對應的自定義組件文件路徑:

js
{
  "usingComponents": {
    "component-tag-name": "path/to/the/custom/component"
  }
}

這樣,在頁面的wxml中就可以像使用基礎組件一樣使用自定義組件節點名即自定義組件的標籤名,節點内容即傳遞給組件的屬性值。

TIP

在app.json中聲明的usingComponents欄位視為全域自定義組件,在小程序內的頁面或自定義組件中可以直接使用而無需再聲明。

代碼示例:

js
<view>
  <!-- The following is a reference to a custom component -->
  <component-tag-name inner-text="Some text"></component-tag-name>
</view>

自定義組件的wxml節點結構在與數據結合之後,將被插入到引用位置內。

1.3 注意事項

一些需要注意的細節:

  • 因為WXML節點標籤名只能是小寫字母、中劃線和底線的組合,所以自定義組件的標籤名也只能包含這些字元。
  • 自定義組件也是可以引用自定義組件的,引用方法類似於頁面引用自定義組件的管道(使用usingComponents欄位)。
  • 自定義組件和頁面所在項目根目錄名不能以'wx-'為首碼,否則會報錯。
注意,是否在分頁檔中使用usingComponents會使得頁面的this對象的原型稍有差异,包括:
  • 使用usingComponents頁面的原型與不使用時不一致,即Object.getPrototypeOf(this)結果不同。
  • 使用usingComponents時會多一些方法,如selectComponent。
  • 出於效能考慮,使用 usingComponents 時, setData 內容不會被直接深複製,即 this.setData({ field: obj }) 後 this.data.field === obj。 (深複製會在這個值被組件間傳遞時發生。)
如果頁面比較複雜,新增或删除usingComponents定義段時建議重新測試一下。

2. 組件模版和樣式

類似於頁面,自定義組件擁有自己的wxml模版和wxss樣式。

2.1 組件模版

組件模版的寫法與頁面模版相同組件模版與組件數據結合後生成的節點樹,將被插入到組件的引用位置上。

在組件模版中可以提供一個<slot>節點,用於承載組件引用時提供的子節點。

代碼示例:

js
<!-- Component template -->
<view class="wrapper">
  <view>This indicates the internal node of the component</view>
  <slot></slot>
</view>
js
<!--Page template where the component is referenced-->
<view>
  <component-tag-name>
    <!-- This part is added to the location of the  <slot>  component -->
    <view>This is inserted in the slot component</view>
  </component-tag-name>
</view>

TIP

在模版中引用到的自定義組件及其對應的節點名需要在json文件中顯式定義,否則會被當作一個無意義的節點除此以外,節點名也可以被聲明為 抽象節點

2.2 模版數據綁定

與普通的WXML模版類似,可以使用數據綁定,這樣就可以向子組件的内容傳遞動態資料。

代碼示例:

js
<!--Page template where the component is referenced-->
<view>
  <component-tag-name prop-a="{{dataFieldA}}" prop-b="{{dataFieldB}}">
    <!-- This part is added to the location of the  <slot>  component -->
    <view>This is inserted in the slot component</view>
  </component-tag-name>
</view>

在以上例子中,組件的内容propA和propB將收到頁面傳遞的數據頁面可以通過setData來改變綁定的數據欄位。

TIP

這樣的數據綁定只能傳遞JSON相容數據

2.3 組件WXML的slot

在組件的wxml中可以包含slot節點,用於承載組件使用者提供的wxml結構。

默認情况下,一個組件的wxml中只能有一個slot需要使用多slot時,可以在組件js中聲明啟用。

js
Component({
  options: {
    multipleSlots: true, // Enable multiple slots in the options of component definition.
  },
  properties: {
    /* ... */
  },
  methods: {
    /* ... */
  },
});

此時,可以在這個組件的wxml中使用多個slot,以不同的name來區分。

js
<!--Page template where the component is referenced-->
<view>
  <component-tag-name>
    <!-- This part is added to the location of the  <slot name="before">  component -->
    <view slot="before">This is inserted in the slot name="before" component</view>
    <!-- This part is added to the location of the  <slot name="after">  component -->
    <view slot="after">This is inserted in the slot name="after" component</view>
  </component-tag-name>
</view>

使用時,用slot内容來將節點插入到不同的slot上。

js
<!--Page template where the component is referenced-->
<view>
  <component-tag-name>
    <!-- This part is added to the location of the  <slot name="before">  component -->
    <view slot="before">This is inserted in the slot name="before" component</view>
    <!-- This part is added to the location of the  <slot name="after">  component -->
    <view slot="after">This is inserted in the slot name="after" component</view>
  </component-tag-name>
</view>

2.4 組件樣式

組件對應wxss文件的樣式,只對組件wxml內的節點生效編寫組件樣式時,需要注意以下幾點:

  • 組件和引用組件的頁面不能使用id選擇器(#a)、内容選擇器([a])和標籤名選擇器,請改用class選擇器。
  • 組件和引用組件的頁面中使用後代選擇器(.a .b)在一些極端情况下會有非預期的表現,如遇,請避免使用。
  • 子元素選擇器(.a>.b)只能用於view組件與其子節點之間,用於其他組件可能導致非預期的情况。
  • 繼承樣式,如font、 color, 會從組件外繼承到組件內。
  • 除繼承樣式外,app.wxss中的樣式、組件所在頁面的樣式對自定義組件無效(除非更改組件樣式隔離選項)。
js
#a { } /* Cannot be used in components */
[a] { } /* Cannot be used in components */
button { } /* Cannot be used in components */
.a > .b { } /* Invalid unless  .a  is  view  component node */

除此以外,組件可以指定它所在節點的默認樣式,使用:host選擇器。

代碼示例:

js
/* Component custom-component.wxss */
:host {
  color: yellow;
}
js
<!-- page's WXML -->
<custom-component>The text here is highlighted in yellow</custom-component>

2.5 外部樣式

有時,組件希望接受外部傳入的樣式類此時可以在Component中用externalClasses定義段定義若干個外部樣式類。

這個特性可以用於實現類似於view組件的hover-class内容:頁面可以提供一個樣式類,賦予view的hover-class,這個樣式類本身寫在頁面中而非view組件的實現中。

TIP

在同一個節點上使用普通樣式類和外部樣式類時,兩個類的優先順序是未定義的,囙此最好避免這種情況。

代碼示例:

js
/* Component custom-component.js */
Component({
  externalClasses: ["my-class"],
});
js
<!-- Component  custom-component.wxml -->
<custom-component class="my-class">The colour of this text is determined by the class outside the component.</custom-component>

這樣,組件的使用者可以指定這個樣式類對應的class,就像使用普通内容一樣。

代碼示例:

js
<!-- page's WXML -->
<custom-component my-class="red-text" />
<custom-component my-class="large-text" />
js
.red-text {
  color: red;
}
.large-text {
  font-size: 1.5em;
}

2.6 使組件接受全域樣式

默認情况下,自定義組件的樣式只受到自定義組件wxss的影響除非以下兩種情况:

  • app.wxss或頁面的wxss中使用了標籤名選擇器(或一些其他特殊選擇器)來直接指定樣式,這些選擇器會影響到頁面和全部組件通常情况下這是不推薦的做法。
  • 在特定的自定義組件啟動了addGlobalClass選項,這使得這個自定義組件能被app.wxss或頁面的wxss中的所有的樣式定義影響。
要啟動addGlobalClass選項,只需要在Component構造器中將options.addGlobalClass欄位置為true。

TIP

當啟動了addGlobalClass選項後,存在外部樣式污染組件樣式的風險,請謹慎選擇。

代碼示例:

js
/* Component  custom-component.js */
Component({
  options: {
    addGlobalClass: true,
  }
}
js
<!-- Component  custom-component.wxml -->
<text class="red-text">
  The colour of this text is determined by the style definitions in `app.wxss` and the page `wxss
</text>
js
/* app.wxss */
.red-text {
  color: red;
}

3.Component構造器

Component構造器可用於定義組件,調用Component構造器時可以指定組件的内容、數據、方法等。

詳細的參數含義和使用請參攷 Component組件

js
Component({
  behaviors: [],

  properties: {
    myProperty: {
      // Property name
      type: String, // Type (required), currently accepted types include: String, Number, Boolean, Object, Array, null (indicating any type)
      value: "", //Property initial value (optional), if not specified, one will be selected based on the type
      observer(newVal, oldVal, changedPath) {
        // The function to be executed when the property is changed (optional) can also be written as a method name string defined in the methods section, such as: '_propertyChange'
        // Usually newVal is the newly set data, oldVal is the old data
      }

    },
    myProperty2: String, // Simplified definition
  },

  data: {}, // Private data, used for template rendering

  lifetimes: {
    // Lifecycle function, which can be a function or a method name defined in the methods field
    attached: function () {},
    moved: function () {},
    detached: function () {},
  },

  // Lifecycle function, which can be a function or a method name defined in the methods field
  attached: function () {}, // The declaration in the attached field is overwritten by that in the lifetimes field
  ready: function () {},

  pageLifetimes: {
    // Component page lifecycle function
    show: function () {},
    hide: function () {},
    resize: function () {},
  },

  methods: {
    onMyButtonTap: function () {
      this.setData({
        // The properties and data are updated in the same way as with the page data
      });
    },
    // The internal method starts with an underscore
    _myPrivateMethod: function () {
      // Here, data.A[0].B is set to 'myPrivateData'
      this.setData({
        "A[0].B": "myPrivateData",
      });
    },
    _propertyChange: function (newVal, oldVal) {},
  },
});

在properties定義段中,屬性名採用駝峰寫法(propertyName);在 wxml 中,指定屬性值時則對應使用連字符寫入法(component-tag-name property-name="attr value"),應用於資料綁定時採用駝峰寫入法(attr="{{propertyName}}")。

3.1 使用Component構造器構造頁面

事實上,小程序的頁面也可以視為自定義組件因而,頁面也可以使用Component構造器構造,擁有與普通組件一樣的定義段與實例方法但此時要求對應json文件中包含usingComponents定義段。

此時,組件的内容可以用於接收頁面的參數,如訪問頁面/pages/index/index? paramA=123&paramB=xyz , 如果聲明有内容paramA或paramB,則它們會被賦值為123或xyz。

頁面的生命週期方法(即on開頭的方法),應寫在methods定義段中。

代碼示例:

js
{
  "usingComponents": {}
}
js
Component({
  properties: {
    paramA: Number,
    paramB: String,
  },

  methods: {
    onLoad: function () {
      this.data.paramA; // Value of page parameter paramA
      this.data.paramB; // Value of page parameter paramB
    },
  },
});

使用Component構造器來構造頁面的一個好處是可以使用behaviors來選取所有頁面中公用的代碼段。

例如,在所有頁面被創建和銷毀時都要執行同一段代碼,就可以把這段代碼選取到behaviors中。

js
// page-common-behavior.js
module.exports = Behavior({
  attached: function () {
    // On page creation
    console.info("Page loaded!");
  },
  detached: function () {
    // When the page is destroyed
    console.info("Page unloaded!");
  },
});
js
// page A
var pageCommonBehavior = require("./page-common-behavior");
Component({
  behaviors: [pageCommonBehavior],
  data: {
    /* ... */
  },
  methods: {
    /* ... */
  },
});
js
// page B
var pageCommonBehavior = require("./page-common-behavior");
Component({
  behaviors: [pageCommonBehavior],
  data: {
    /* ... */
  },
  methods: {
    /* ... */
  },
});

4. 組件間通信與事件

4.1 組件間通信

組件間的基本通信方式有以下幾種

  • WXML數據綁定:用於父組件向子組件的指定内容設定數據,僅能設定JSON相容數據具體在 組件模版和樣式
  • 事件:用於子組件向父組件傳遞數據,可以傳遞任意數據。
  • 如果以上兩種方式不足以滿足需要,父組件還可以通過this.selectComponent方法獲取子組件實例對象,這樣就可以直接訪問組件的任意數據和方法。

4.2 監聽事件

事件系統是組件間通信的主要管道之一自定義組件可以觸發任意的事件,引用組件的頁面可以監聽這些事件關於事件的基本概念和用法,參見 事件

監聽自定義組件事件的方法與監聽基礎組件事件的方法完全一致:

代碼示例:

js
<!-- The "onMyEvent" method is called when the custom component triggers "myevent" -->
<component-tag-name bindmyevent="onMyEvent" />
<!-- Or write it like this -->
<component-tag-name bind:myevent="onMyEvent" />
js
Page({
  onMyEvent: function (e) {
    e.detail; // The detail object provided when the custom component triggers an event
  },
});

4.3 觸發事件

自定義組件觸發事件時,需要使用triggerEvent方法,指定事件名、detail對象和事件選項:

代碼示例:

js
<!-- In the custom component -->
<button bindtap="onTap">Click this button to trigger the "myevent" event</button>
js
Component({
  properties: {},
  methods: {
    onTap: function () {
      var myEventDetail = {}; // detail object, provided to the event monitoring function
      var myEventOption = {}; // Event triggering options
      this.triggerEvent("myevent", myEventDetail, myEventOption);
    },
  },
});

觸發事件的選項包括:

選項名
類型
是否必填
預設值
描述
bubbles
Boolean
false
事件是否冒泡
composed
Boolean
false
事件是否可以穿越組件邊界,為false時,事件將只能在引用組件的節點樹上觸發,不進入其他任何組件內部
capturePhase
Boolean
false
事件是否擁有捕獲階段

關於冒泡和捕獲階段的概念,請閱讀 事件詳解 章節中的相關說明。

代碼示例:

js
// page page.wxml
<another-component bindcustomevent="pageEventListener1">
  <my-component bindcustomevent="pageEventListener2"></my-component>
</another-component>
js
// Component  another-component.wxml
<view bindcustomevent="anotherEventListener">
  <slot />
</view>
js
// Component  my-component.wxml
<view bindcustomevent="myEventListener">
  <slot />
</view>
js
// Component  my-component.js
Component({
  methods: {
    onTap: function () {
      this.triggerEvent("customevent", {}); // Only pageEventListener2 is triggered
      this.triggerEvent("customevent", {}, { bubbles: true }); // pageEventListener2 and pageEventListener1 are triggered in sequence
      this.triggerEvent("customevent", {}, { bubbles: true, composed: true }); // pageEventListener2, anotherEventListener, and pageEventListener1 are triggered in sequence
    },
  },
});

4.4 獲取組件實例

可在父組件裏調用this.selectComponent,獲取子組件的實例對象。

調用時需要傳入一個匹配選擇器selector,如: this.selectComponent('.my-component')。

selector詳細語法可查看 selector語法參攷文件

代碼示例:

js
// parent component
Page({
  data: {},
  getChildComponent: function () {
    const child = this.selectComponent(".my-component");
    console.log(child);
  },
});

在上例中,父組件將會獲取class為my-component的子組件實例對象,即子組件的this。

若需要自定義selectComponent返回的數據,可使用內寘behavior: wx://component-export。

使用該behavior時,自定義組件中的export定義段將用於指定組件被selectComponent調用時的返回值。

代碼示例:

js
// Custom Components my-component Internal
Component({
  behaviors: ["wx://component-export"],
  export() {
    return { myField: "myValue" };
  },
});
js
<! -- When using custom components -->
<my-component id="the-id" />
js
// Called by the parent component
const child = this.selectComponent("#the-id"); // be tantamount to { myField: 'myValue' }

在上例中,父元件取得 id 為 the-id 的子元件實例的時候,得到的是對象{ myField: 'myValue' }。

5. 組件生命週期

組件的生命週期,指的是組件自身的一些函數,這些函數在特殊的時間點或遇到一些特殊的框架事件時被自動觸發。

其中,最重要的生命週期是created attached detached,包含一個組件實例生命流程的最主要時間點。

  • 組件實例剛剛被創建好時, created生命週期被觸發此時,組件數據this.data就是在Component構造器中定義的數據data此時還不能調用setData通常情况下,這個生命週期只應該用於給組件this添加一些自訂屬性欄位。
  • 在組件完全初始化完畢、進入頁面節點樹後,attached生命週期被觸發此時,this.data已被初始化為組件的當前值這個生命週期很有用,絕大多數初始化工作可以在這個時機進行。
  • 在組件離開頁面節點樹後, detached生命週期被觸發退出一個頁面時,如果組件還在頁面節點樹中,則detached會被觸發。

5.1 定義生命週期方法

生命週期方法可以直接定義在Component構造器的第一級參數中。

組件的生命週期也可以在lifetimes欄位內進行聲明(這是推薦的管道,其優先順序最高)。

代碼示例:

js
Component({
  lifetimes: {
    attached: function () {
      // Executed when the component instance enters the page node tree
    },
    detached: function () {
      // Executed when the component instance is removed from the page node tree
    },
  },
  // The older definition methods below can be used for compatibility with base library versions under 2.2.3
  attached: function () {
    // Executed when the component instance enters the page node tree
  },
  detached: function () {
    // Executed when the component instance is removed from the page node tree
  },
  // ...
});

在behaviors中也可以編寫生命週期方法,同時不會與其他behaviors中的同名生命週期相互覆蓋但要注意,如果一個組件多次直接或間接引用同一個behavior,這個behavior中的生命週期函數在一個執行時機內只會執行一次。

可用的全部生命週期如下表所示

生命週期
參數
描述
created
組件生命週期函數-在組件實例剛剛被創建時執行,注意此時不能調用setData
attached
組件生命週期函數-在組件實例進入頁面節點樹時執行
ready
組件生命週期函數-在組件佈局完成後執行
moved
組件生命週期函數-在組件實例被從頁面節點樹移除時執行
detached
組件生命週期函數-在組件實例被從頁面節點樹移除時執行
error
Object Error
組件間關係定義,參見

5.2 組件所在頁面的生命週期函數

生命週期
參數
描述
show
組件所在的頁面被展示時執行
hide
組件所在的頁面被隱藏時執行
resize
Object Size
組件所在的頁面尺寸變化時執行

說明

自定義tabBar的pageLifetime不會觸發。

代碼示例:

js
Component({
  pageLifetimes: {
    show: function () {
      // Page displayed
    },
    hide: function () {
      // Page hidden
    },
    resize: function (size) {
      // Page resized
    },
  },
});

6.behaviors

behaviors是用於組件間代碼共亯的特性,類似於一些程序設計語言中的'mixins'或'traits'。

每個behavior可以包含一組内容、數據、生命週期函數和方法組件引用它時,它的内容、數據和方法會被合併到組件中,生命週期函數也會在對應時機被調用每個組件可以引用多個behavior,behavior也可以引用其它behavior。

詳細的參數含義和使用請參攷 Behavior 參攷文件。

6.1 組件中使用

behavior需要使用Behavior()構造器定義。

代碼示例:

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

組件引用時,在behaviors定義段中將它們逐個列出即可。

js
// my-component.js
var 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
當組件觸發生命週期時,上例生命週期函數執行順序為:
  1. [my-behavior] created
  2. [my-component] created
  3. [my-behavior] attached
  4. [my-component] attached
  5. [my-behavior] ready
  6. [my-component] ready

詳細規則參攷 同名欄位的覆蓋和組合規則

6.2 同名欄位的覆蓋和組合規則

組件和它引用的behavior中可以包含同名的欄位,對這些欄位的處理方法如下:

  • 如果有同名的内容(properties)或方法(methods):
    • 若組件本身有這個内容或方法,則組件的内容或方法會覆蓋behavior中的同名内容或方法;
    • 若組件本身無這個内容或方法,則在組件的behaviors欄位中定義靠後的behavior的内容或方法會覆蓋靠前的同名内容或方法;
    • 在2的基礎上,若存在嵌套引用behavior的情况,則規則為:引用者behavior覆蓋被引用的behavior中的同名内容或方法。
  • 如果有同名的數據欄位(data):
    • 若同名的數據欄位都是對象類型,會進行對象合併;
    • 其餘情况會進行數據覆蓋,覆蓋規則為:引用者behavior >被引用的behavior、靠後的behavior >靠前的behavior(優先順序高的覆蓋優先順序低的,最大的為優先順序最高)。
  • 生命週期函數和observers不會相互覆蓋,而是在對應觸發時機被逐個調用:
    • 對於不同的生命週期函數之間,遵循組件生命週期函數的執行順序;
    • 對於同種生命週期函數和同欄位observers,遵循如下規則:
      • behavior優先於組件執行;
      • 被引用的behavior優先於引用者behavior執行;
      • 靠前的behavior優先於靠後的behavior執行;
    • 如果同一個behavior被一個組件多次引用,它定義的生命週期函數和observers不會重複執行。

6.3 內寘behaviors

自定義組件可以通過引用內寘的behavior來獲得內寘組件的一些行為。

js
Component({
  behaviors: ["wx://form-field"],
});

在上例中, wx://form-field代表一個內寘behavior,它使得這個自定義組件有類似於表單控制項的行為。

內寘behavior往往會為組件添加一些内容在沒有特殊說明時,組件可以覆蓋這些内容來改變它的type或添加observer。

6.3.1 wx://form-field

使自定義組件有類似於表單控制項的行為form組件可以識別這些自定義組件,並在submit事件中返回組件的欄位名及其對應欄位值。

内容名類型描述
nameString在表單中的欄位名
value任意在表單中的欄位值

代碼示例:

js
// custom-form-field.js
Component({
  behaviors: ["wx://form-field"],
  data: {
    value: "",
  },
  methods: {
    onChange: function (e) {
      this.setData({
        value: e.detail.value,
      });
    },
  },
});

6.3.2 wx://form-field

使自定義組件支持export定義段這個定義段可以用於指定組件被selectComponent調用時的返回值。

詳細用法以及代碼示例可見: selectComponent參攷文件

7.組件間關係

7.1定義和使用組件間關係

有時需要實現這樣的組件:

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

這個例子中,custom-ul和custom-li都是自定義組件,它們有相互間的關係,相互間的通信往往比較複雜此時在組件定義時加入relations定義段,可以解决這樣的問題示例:

js
// path/to/custom-ul.js
Component({
  relations: {
    "./custom-li": {
      type: "child", // The linked target node should be a child node
      linked: function (target) {
        // Executed each time custom-li is inserted. "target" is the instance object of this node, which is triggered after the attached lifecycle of this node.
      },
      linkChanged: function (target) {
        // Executed each time custom-li is moved. "target" is the instance object of this node, which is triggered after the moved lifecycle of this node.
      },
      unlinked: function (target) {
        // Executed each time custom-li is removed. "target" is the instance object of this node, which is triggered after the detached lifecycle of this node.
      },
    },
  },
  methods: {
    _getAllLi: function () {
      // You can get the nodes array via getRelationNodes, including all linked custom-li nodes listed in order
      var nodes = this.getRelationNodes("path/to/custom-li");
    },
  },
  ready: function () {
    this._getAllLi();
  },
});
js
// path/to/custom-li.js
Component({
  relations: {
    "./custom-ul": {
      type: "parent", // The linked target node should be a parent node
      linked: function (target) {
        // Executed after each insertion to custom-ul. "target" is the instance object of the custom-ul node, which is triggered after the attached lifecycle.
      },
      linkChanged: function (target) {
        // Executed after each move. "target" is the instance object of the custom-ul node, which is triggered after the moved lifecycle.
      },
      unlinked: function (target) {
        // Executed after each removal. "target" is the instance object of the custom-ul node, which is triggered after the detached lifecycle.
      },
    },
  },
});

TIP

必須在兩個組件定義中都加入relations定義,否則不會生效。

7.2 關聯一類組件

有時,需要關聯的是一類組件,如:

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

custom-form組件想要關聯custom-input和custom-submit兩個組件此時,如果這兩個組件都有同一個behavior:

js
// path/to/custom-form-controls.js
module.exports = Behavior({
  // ...
});
js
// path/to/custom-input.js
var customFormControls = require("./custom-form-controls");
Component({
  behaviors: [customFormControls],
  relations: {
    "./custom-form": {
      type: "ancestor", // he linked target node should be an ancestor node
    },
  },
});
js
// path/to/custom-submit.js
var customFormControls = require("./custom-form-controls");
Component({
  behaviors: [customFormControls],
  relations: {
    "./custom-form": {
      type: "ancestor", // The linked target node should be an ancestor node
    },
  },
});

則在relations關係定義中,可使用這個behavior來代替組件路徑作為關聯的目標節點:

js
// path/to/custom-form.js
var customFormControls = require("./custom-form-controls");
Component({
  relations: {
    customFormControls: {
      type: "descendant", // The linked target node should be a descendant node
      target: customFormControls,
    },
  },
});

7.3relations定義段

relations定義段包含目標組件路徑及其對應選項,可包含的選項見下錶。

選項名類型是否必填描述
typeString目標組件的相對關係,可選的值為parent、 child 、 ancestor 、 descendant
linkedFunction關係生命週期函數,當關係被建立在頁面節點樹中時觸發,觸發時機在組件attached生命週期之後
linkChangedFunction關係生命週期函數,當關係在頁面節點樹中發生改變時觸發,觸發時機在組件moved生命週期之後
unlinkedFunction關係生命週期函數,當關係脫離頁面節點樹時觸發,觸發時機在組件detached生命週期之後
targetFunction如果這一項被設定,則它表示關聯的目標節點所應具有的behavior,所有擁有這一behavior的組件節點都會被關聯

8.數據監聽器

數據監聽器可以用於監聽和響應任何内容和數據欄位的變化。

8.1使用數據監聽器

有時,在一些數據欄位被setData設定時,需要執行一些操作。

例如, this.data.sum永遠是this.data.numberA與this.data.numberB的和此時,可以使用數據監聽器進行如下實現

js
Component({
  attached: function() {
    this.setData({
      numberA: 1,
      numberB: 2,
    })
  },
  observers: {
    'numberA, numberB': function(numberA, numberB) {
      // Execute this function when numberA or numberB is set
      this.setData({
        sum: numberA + numberB
      })
    }
  }
})

8.2監聽欄位語法

數據監聽器支持監聽内容或內部數據的變化,可以同時監聽多個一次setData最多觸發每個監聽器一次。

同時,監聽器可以監聽子數據欄位,如下例所示。

js
Component({
  observers: {
    'some.subfield': function(subfield) {
      // Triggered when this.data.some.subfield is set via setData
      // (Also triggered when this.data.some is set via setData)
      subfield === this.data.some.subfield
    },
    'arr[12]': function(arr12) {
      // Triggered when this.data.arr[12] is set via setData
      // (Also triggered when this.data.arr is set via setData)
      arr12 === this.data.arr[12]
    },
  }
})

如果需要監聽所有子數據欄位的變化,可以使用萬用字元**。

js
Component({
  observers: {
    'some.field.**': function(field) {
      // Triggered when setData is used to set this.data.some.field itself or any of its sub-data fields
      // (Also triggered when this.data.some is set via setData)
      field === this.data.some.field
    },
  },
  attached: function() {
    // This will trigger the above observer
    this.setData({
      'some.field': { /* ... */ }
    })
    // This will also trigger the above observer
    this.setData({
      'some.field.xxx': { /* ... */ }
    })
    // This will still trigger the above observer
    this.setData({
      'some': { /* ... */ }
    })
  }
})

特別地,僅使用萬用字元**可以監聽全部setData。

js
Component({
  observers: {
    '**': function() {
      // Triggered upon each setData operation
    },
  },
})

8.3 注意事項

  • 數據監聽器監聽的是setData涉及到的數據欄位,即使這些數據欄位的值沒有發生變化,數據監聽器依然會被觸發;
  • 如果在數據監聽器函數中使用setData設定本身監聽的數據欄位,可能會導致死迴圈,需要特別留意;
  • 數據監聽器和内容的observer相比,數據監聽器更强大且通常具有更好的效能。

9.純數據欄位

純數據欄位是一些不用於介面渲染的data欄位,可以用於提升頁面更新效能。

9.1組件數據中的純數據欄位

有些情况下,某些data中的欄位(包括setData設定的欄位)既不會展示在介面上,也不會傳遞給其他組件,僅僅在當前組件內部使用。

此時,可以指定這樣的數據欄位為'純數據欄位',它們將僅僅被記錄在this.data中,而不參與任何介面渲染過程,這樣有助於提升頁面更新效能。

指定'純數據欄位'的方法是在Component構造器的options定義段中指定pureDataPattern為一個規則運算式,欄位名符合這個規則運算式的欄位將成為純數據欄位。

代碼示例:

js
Component({
  options: {
    pureDataPattern: /^_/ // Specify all data fields starting with _ as pure data fields
  },
  data: {
    a: true, // Plain data field
    _b: true, // Plain data field
  }, methods: { a: true, // Normal data field _b: true, // Plain data field
  methods: {
    myMethod() {
      this.data._b // Plain data fields can be retrieved in this.data
      this.setData({
        c: true, // Plain data field
        _d: true, // Plain data field
      })
    }
  }
})

上述組件中的純數據欄位不會被應用到WXML上:

js
<view wx:if="{{a}}"> This line will be displayed </view>
<view wx:if="{{_b}}"> This line will not be shown </view>

9.2組件内容中的純數據欄位

内容也可以被指定為純數據欄位(遵循pureDataPattern的規則運算式)。

内容中的純數據欄位可以像普通内容一樣接收外部傳入的屬性值,但不能將它直接用於組件自身的WXML中。

代碼示例:

js
Component({
  options: {
    pureDataPattern: /^_/
  },
  properties: {
    a: Boolean,
    _b: {
      type: Boolean,
      observer() {
        // Don't do this! This observer will never be triggered
      }
    },
  }
})

TIP

内容中的純數據欄位的内容observer永遠不會觸發! 如果想要監聽屬性值變化,使用 數據監聽器 數據監聽器

9.3 使用數據監聽器監聽純數據欄位

數據監聽器 可以用於監聽純數據欄位(與普通數據欄位一樣)這樣,可以通過監聽、響應純數據欄位的變化來改變介面。

下麵的示例是一個將JavaScript時間戳記轉換為可讀時間的自定義組件。

代碼示例:

js
Component({
  options: {
    pureDataPattern: /^timestamp$/ // Specifying the timestamp attribute as a data-only field
  },
  properties: {
    timestamp: Number,
  },
  observers: {
    timestamp: function () {
      // When timestamp is set, display it as a readable time string.
      var timeString = new Date(this.data.timestamp).toLocaleString()
      this.setData({
        timeString: timeString
      })
    }
  }
})
js
<view>{{timeString}}</view>

10.抽象節點

10.1在組件中使用抽象節點

有時,自定義組件模版中的一些節點,其對應的自定義組件不是由自定義組件本身確定的,而是自定義組件的調用者確定的這時可以把這個節點聲明為'抽象節點'。

例如,我們現在來實現一個'選框組'(selectable-group)組件,它其中可以放置單選框(custom-radio)或者核取方塊(custom-checkbox)這個組件的wxml可以這樣編寫:

代碼示例:

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

其中,'selectable'不是任何在json文件的usingComponents欄位中聲明的組件,而是一個抽象節點它需要在componentGenerics欄位中聲明:

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

10.2使用包含抽象節點的組件

在使用selectable-group組件時,必須指定'selectable'具體是哪個組件:

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

這樣,在生成這個selectable-group組件的實例時,'selectable'節點會生成'custom-radio'組件實例類似地,如果這樣使用:

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

selectable'節點則會生成'custom-checkbox'組件實例。

TIP

上述的custom-radio和custom-checkbox需要包含在這個wxml對應json文件的usingComponents定義段中。

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

10.3抽象節點的默認組件

抽象節點可以指定一個默認組件,當具體組件未被指定時,將創建默認組件的實例默認組件可以在componentGenerics欄位中指定:

js
{
  "componentGenerics": {
    "selectable": {
      "default": "path/to/default/component"
    }
  }
}

10.4注意事項

節點的generic引用generic:xxx='yyy'中,值yyy只能是靜態值,不能包含數據綁定因而抽象節點特性並不適用於動態决定節點名的場景。

11.自定義組件擴展

為了更好定制自定義組件的功能,可以使用自定義組件擴展機制。

11.1自定義組件擴展

為了更好的理解擴展後的效果,先舉一個例子:

js
// 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
  }
})

通過例子可以發現,自定義組件的擴展其實就是提供了修改自定義組件定義段的能力,上述例子就是修改了自定義組件中的data定義段裡的內容。

11.2使用擴展

Behavior()構造器提供了新的定義段definitionFilter,用於支持自定義組件擴展definitionFilter是一個函數,在被調用時會注入兩個參數,第一個參數是使用該behavior的component/behavior的定義對象,第二個參數是該behavior所使用的behavior的definitionFilter函數清單。

以下舉個例子來說明:

js
// 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的定義對象。

代碼示例:

下麵利用擴展簡單實現自定義組件的計算内容功能:

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

在組件中使用:

js
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,
      })
    }
  }
})
js
<view>data: {{a}}</view>
<view>computed: {{b}}</view>
<button bindtap="onTap">click</button>

實現原理很簡單,對已有的setData進行二次封裝,在每次setData的時候計算出computed裏各欄位的值,然後設到data中,以達到計算内容的效果。

12.占位組件

在使用如 分包非同步化 特性時,自定義組件所引用的其他自定義組件,在剛開始進行渲染時可能處於不可用的狀態此時,為了使渲染過程不被阻塞,不可用的自定義組件需要一個「占位組件」(Component placeholder)基礎庫會用占位組件替代不可用組件進行渲染,在該組件可用後再將占位組件替換回該組件。

一個自定義組件的占位組件可以是另一個自定義組件、或一個內寘組件。

12.1配置

頁面或自定義組件對應的JSON配置中的componentPlaceholder欄位用於指定占位組件,如:

js
{
  "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中配置)
假設該配置對應的模版如下:
js
<button ontap="onTap">display component</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均不可用,則頁面將被渲染為:

js
<button>display component</button>
<view>
  <comp-c prop="{{ p }}">text in slot</comp-c>
</view>

comp-a與comp-b準備完成後,頁面被替換為:

js
<button>display component</button>
<comp-a>
  <comp-b prop="{{ p }}">text in slot</comp-b>
</comp-a>

12.2注意事項

  1. 當一個組件被指定為占位組件時(如上例中的comp-c),為其指定占位組件是無效的可以理解為如果一個組件需要作為其他組件的占位組件,則它必須在一開始就是可用的;
  2. 現時自定義組件不可用的情况包括:
    2.1. 使用分包非同步化特性的情况下,引用了其他分包的組件,而對應分包還未下載;
    2.2. 使用時注入特性的情况下,該組件還未注入;
  3. 如果一個組件不可用,且其占位組件不存在,則渲染時會報錯並拋出;
  4. 如果一個組件不存在,但為其指定了可用的占位組件,則占位組件可以被正常渲染,但後續嘗試準備替換時會報錯並拋出。

附:有占位組件參與的渲染流程

基礎庫嘗試渲染一個組件時,會首先遞迴檢查usingComponents,收集其將使用到的所有組件的資訊; 在這個過程中,如果某個被使用到的組件不可用,基礎庫會先檢查其是否有對應的占位組件如果沒有,基礎庫會中斷渲染並拋出錯誤; 如果有,則會標記並在後續渲染流程中使用占位組件替換該不可用的組件進行渲染不可用的組件會在當前渲染流程結束後嘗試準備(下載分包或注入代碼等); 等到準備過程完成後,再嘗試渲染該組件(實際上也是在執行這個流程),並替換掉之前渲染的占位組件。