這是我參與更文挑戰的第4天,活動詳情查看: 更文挑戰前端
自定義組件可以幫咱們更好的複用代碼和重構簡化代碼複雜度。從小程序基礎庫版本 1.6.3 開始,小程序支持簡潔的組件化編程。全部自定義組件相關特性都須要基礎庫版本 1.6.3 或更高。開發者能夠將頁面內的功能模塊抽象成自定義組件,以便在不一樣的頁面中重複使用;也能夠將複雜的頁面拆分紅多個低耦合的模塊,有助於代碼維護。自定義組件在使用時與基礎組件很是類似。node
小程序自定義組件的開發細節的核心點:web
-組件的生命週期;編程
在這個過程當中會結合組件化思想,以及 Web Components 規範輔助理解。Web Components 規範是 W3C 推出的一套用於封裝具備複用性、互動性前端組件的技術規範,旨在提供一種標準的組件化模式。 目前流行的幾個前端框架( Vue / React / Angular )都在必定程度上遵循了這套規範,微信小程序的自定義組件也是如此,並且,微信小程序渲染自定義組件使用的是 Shadow DOM,這項技術是 Web Components 規範的一部分。 做爲一名前端開發者,你可能經歷過 BootStrap 盛行的年代,也多是從更悠久的 ExtJS 時代一路走來,就算你不瞭解這兩個框架,確定不可避免地使用過 React、Vue 或 Angular 這三種框架。React / Vue / Angular 與它們的前輩 BootStrap / ExtJS 有一點是共通的:它們都是前端組件化的推崇者。 能夠把「前端組件化」理解爲「面向對象編程思想在前端 UI 領域的具體實踐,將部分 UI 內容抽離爲獨立的可複用的組件」,這樣作有這樣 3 點優點:json
這是組件化最直接的優勢,若是不用組件,每次遇到相同的業務場景都須要從新編寫代碼,而被抽離後的組件能夠在其適用的場景內被重複使用,很大程度上下降了開發耗時。小程序
想象一下,假如一個頁面的全部 UI 源碼都集中在同一個 HTML 文件中,當頁面中的導航欄出現 Bug,你須要在上千行甚至上萬行的 HTML 文件中找到導航欄對應的 HTML 標籤。若是將導航欄抽離爲一個組件,那麼你僅僅須要在這個組件內尋找。這類案例在工做中廣泛存在,經過這個例子能夠充分說明組件化在下降代碼維護難度方面的優點。微信小程序
《重構:改善既有代碼的設計》講了:重構並非當系統複雜度提高到必定程度難以維護時的一次性行爲,而是一種高頻的、小規模的平常行爲。直白點兒說就是:應該不斷經過重構來改善系統,無論重構的範圍有多小。可是這種實踐方式對於代碼的可維護性有很大的挑戰,這也間接說明了組件化對重構工做的正面影響:經過提升代碼的可維護性,間接下降了系統的重構難度。數組
Component像頁面同樣由wxml、wxss、js和json4個文件組成,且須要把這4個文件放在同一個目錄中。與頁面不同的是,Component中的構造函數(也能夠稱構造器)是Component({}),而頁面中的構造函數是Page({})。要編寫一個自定義組件,首先須要在 json 文件中進行自定義組件聲明(將component字段設爲true可這一組文件設爲自定義組件):瀏覽器
{
"component": true
}
複製代碼
Component的slot(slot意思是插槽),主要是讓你在外部的wxml能夠自由的在你的Component的wxml裏插入模塊。默認狀況下,一個組件的wxml只可能有一個slot。須要使用多個時,能夠在組件js中聲明啓用。前端框架
Component({
options: {
multipleSlots: true // 在組件定義時的選項中啓用多slot支持
},
properties: { /* ... */ },
methods: { /* ... */ }
})
複製代碼
此時,能夠在這個組件的wxml中使用多個slot,以不一樣的 name 來區分。
<!-- 組件模板 -->
<view class="wrapper"> <slot name="before"></slot> <view>這裏是組件的內部細節</view> <slot name="after"></slot> </view>
複製代碼
使用時,用 slot 屬性來將節點插入到不一樣的slot上。
<!-- 引用組件的頁面模板 -->
<view>
<component-tag-name>
<!-- 這部份內容將被放置在組件 <slot name="before"> 的位置上 -->
<view slot="before">這裏是插入到組件slot name="before"中的內容</view>
<!-- 這部份內容將被放置在組件 <slot name="after"> 的位置上 -->
<view slot="after">這裏是插入到組件slot name="after"中的內容</view>
</component-tag-name>
</view>
複製代碼
組件和引用組件的頁面不能使用id選擇器(#a)、屬性選擇器([a])和標籤名選擇器,請改用class選擇器。組件和引用組件的頁面中使用後代選擇器(.a .b)在一些極端狀況下會有非預期的表現,如遇,請避免使用。子元素選擇器(.a>.b)只能用於 view 組件與其子節點之間,用於其餘組件可能致使非預期的狀況。
繼承樣式,如 font 、 color ,會從組件外繼承到組件內。除繼承樣式外, app.wxss 中的樣式、組件所在頁面的的樣式對自定義組件無效 (小程序頻繁報大量的警告)。
#a { } /* 在組件中不能使用 */
[a] { } /* 在組件中不能使用 */
button { } /* 在組件中不能使用 */
.a > .b { } /* 除非 .a 是 view 組件節點,不然不必定會生效 */
複製代碼
使用外部樣式類可讓組件使用指定的組件外樣式類,若是但願組件外樣式類可以徹底影響組件內部,能夠將組件構造器中的options.addGlobalClass字段置爲true。
/* 組件 custom-component.js */
Component({
externalClasses: ['my-class']
})
<!-- 組件 custom-component.wxml -->
<custom-component class="my-class">這段文本的顏色由組件外的 class 決定</custom-component>
/* 組件外的樣式定義 */
.red-text {
color: red;
}
複製代碼
建立一個組件
<!--components/component/component.wxml-->
<view class="inner"> {{innerText}} </view>
<slot></slot>
複製代碼
編寫JS文件,組件的屬性值和內部數據將被用於組件 wxml 的渲染,其中,屬性值是可由組件外部傳入的
// components/component/component.js
Component({
/** * 組件的屬性列表 */
properties: {
innerText: {
type: String,
value: 'hello world'
},
myProperties:String
},
/** * 組件的初始數據 */
data: {
},
/** * 組件的方法列表 */
methods: {
}
})
複製代碼
設置字體的顏色
/* components/component/component.wxss */
.inner{color: red;}
複製代碼
完成對組件的初始化,包括設置屬性列表,初始化數據,以及設置相關的方法。
使用已註冊的自定義組件前,首先要在頁面的 json 文件中進行引用聲明。此時須要提供每一個自定義組件的標籤名和對應的自定義組件文件路徑:
{
"usingComponents": {
"component": "/components/component/component"
}
}
複製代碼
在page頁面下添加聲明過的自定義組件:
// <component></component>
<view>
<component> <!-- 這部份內容將被放置在組件 <slot> 的位置上 --> <view>這裏是插入到組件slot中的內容</view> </component> </view>
複製代碼
上方的是一個最簡單的自定義組件。在開發微信小程序自定義組件的三個核心環節中咱們須要注意如下幾個細節:
建立微信小程序自定義組件須要使用 Component 構造器,這是微信小程序結構體系內最小粒度的構造器,外層是 Page 構造器,最外層的是 App 構造器,三者的關係以下圖:
從外到內依次是 App > Page > Component,每次遞進是 1:N 的關係:
1 個 App(也就是 1 個小程序)可包含 N( N >= 1 )個 Page;
1 一個 Page 可包含N(N>=1)個 Component。
每一個自定義組件的資源必須包括四個基本文件:
用於描述組件結構的 wxml 文件;
用於描述組件樣式的 wxss 文件;
用於描述組件行爲的 js 文件;
用於聲明組件配置的 json 文件。
跟傳統前端開發相比,小程序自定義組件的 wxml 和 wxss 文件的編寫方式與 HTML 和 CSS 編寫基本相似,不要特別關注,差別性主要體如今 js 和 json 文件上,在 json 文件中必須經過 component 字段聲明此組件爲自定義組件,以下:
{
"component": true
}
複製代碼
js 文件中經過 Component 構造器建立組件的邏輯實體,以下:
Component({
behaviors:[],
properties:{},
data: {},
lifetimes: {},
pageLifetimes: {},
methods: {}
});
複製代碼
對照 Vue 和 React 講解 Component 構造器的幾個屬性更容易理解: behaviors 相似於 Vue 和 React 中的 mixins,用於定義多個組件之間的共享邏輯,能夠包含一組 properties、data、lifetimes 和 methods 的定義;
properties
相似於 Vue 和 React 中的 props ,用於接收外層(父組件)傳入的數據;
data
相似於 Vue 中的 data 以及 React 中的 state ,用於描述組件的私用數據(狀態);
lifetimes
用於定義組件自身的生命週期函數,這種寫法是從小程序基礎庫 2.2.3 版本引入的,本來的寫法與 Vue 和 React 相似,都是直接掛載到組件的一級屬性上(下一小節咱們將詳細講解生命週期函數的相關知識);
pageLifetimes
是微信小程序自定義組件首創的一套邏輯,用於監聽此組件所在頁面的生命週期。通常用於在頁面特定生命週期時改變組件的狀態,好比在頁面展現時(show)把組件的狀態設置爲 A,在頁面隱藏時(hide)設置爲 B;
methods 與 Vue 的 methods 相似,用於定義組件內部的函數。
除 4 個基礎文件之外,自定義組件還能夠包含一些其餘必要的資源,好比圖片,下圖展現的是自定義組件 chatroom 的資源列表:你能夠看到,除了 wxml/wxss/js/json 文件之外,還有兩個圖片文件,在 wxml 中能夠直接使用相對目錄引用,
<image src="./photo.png"></image>
複製代碼
而對一個組件來講,生命週期指的是這個組件從被建立到銷燬的過程,在這個過程當中的里程碑階段暴露出一些鉤子函數,方便開發者針對不一樣階段編寫邏輯,這些函數就是所謂的「生命週期函數」。微信小程序自定義組件的生命週期函數有如下幾個:
跟 Vue 和 React 相比,小程序自定義組件的生命週期更貼近 Web Components 規範。因此接下來咱們結合 Web Components 規範來理解小程序自定義組件的生命週期。 Web Components 規範引入了一個概念:自定義 HTML 元素。目的跟小程序相似,都是爲了建立一種自定義的 UI 組件。瀏覽器環境中,每一個 HTML 標籤都存在一個對應的類(Class),好比段落節點 對應 HTMLParagraphElement 類,繼承這個類所建立的元素即是自定義 HTML 元素,以下代碼:
// 建立自定義元素
class MyCustomParagraphElement extends HTMLParagraphElement {
//...
}
// 註冊自定義元素
customElements.define('custom-p', MyCustomParagraphElement);
複製代碼
自定義元素必須被註冊(或者叫做定義)以後才能夠被使用,上述代碼的最後一行即是註冊邏輯,第一個參數是該元素被註冊後的 HTML 標籤名稱。註冊成功後即可以直接在 HTML 中使用該元素,以下:
<custom-p></custom-p>
複製代碼
這個流程與微信小程序的自定義組件很是類似,只不過註冊組件的行爲是由小程序底層處理的,開發者僅須要編寫組件自己的代碼就能夠了。 Web Components 規範對於自定義 HTML 元素的生命週期描述爲下圖所示的流程:
對比 Web Components 規範和小程序自定義組件的生命週期,二者有必定類似之處但並不徹底一致,總結出這樣幾點:
小程序自定義組件的 attached 和 detached 函數分別對應 Web Components 規範的connectedCallback 和 disconnectedCallback,功能上是一致的;
小程序自定義組件的 moved 函數與 Web Components 規範的 adoptedCallback 相似但做用並不徹底相同。因爲小程序不支持 iframe,因此不存在組件在文檔範疇上的遷移,只能在同一個文檔的不一樣父節點之間遷移。因此也就不存在 adopted 狀態,moved 函數能夠理解爲adopted 的一種變體;
小程序自定義組件獨有的生命週期函數,created、ready 和 error;
Web Components 規範獨有的生命週期函數,attributeChangedCallback。
可見小程序自定義組件與 Web Components 規範的主要差別體如今第 3 點和第 4 點。爲何會有這樣的差別呢?
差別點一:爲何小程序的自定義組件沒有attributeChangedCallback函數?
首先咱們要明確 attributeChangedCallback 函數的觸發時機,Web Components 規範對這個函數的描述爲「當自定義元素的任一屬性發生改變(包括新增、刪除、更新)時觸發」。而更新元素屬性這種行爲是傳統 DOM 編程中常見的,在目前倡導數據驅動 UI 的背景下,絕大多數框架都是經過 VDOM 來間接操做 DOM,因此更新屬性在目前的時代背景下很是少見。
微信小程序與 Vue/React 同樣,一樣不容許直接操做 DOM,從根本上就不可能發生 DOM 屬性改變的狀況。這就解釋了爲什麼小程序自定義組件的生命週期中沒有 attributeChangedCallback 函數。
差別點二:Web Components 規範爲什麼沒有 created/ready/error 三個函數?
技術規範是一種指導方針,具體的實現方式每每須要根據現實狀況決定;Web Components 規範一樣如此,它脫離於業務,單純從技術的角度提供了最基礎的標準和參考,具體到實現層面,Vue/React 之類的框架有各自的理解,微信小程序一樣也有獨到之處。
之因此有差別,一方面是出於各框架開發者對規範的理解和延伸,另外一方面是考慮到實際的業務須要,因此每每會有一些規範未覆蓋的「創新」之處,最典型的就是 document.ready 事件。在DOMContentLoad 規範推出以前,jQuery 的 $(document).ready 事件已經在前端技術圈盛行了好久,這個事件發生了 window.onload 以前,此時的文檔狀態處於渲染未完成可是可交互,因此這個事件在優化網站性能的 FIT(First Load Time,提升加載速度)方面被頻繁使用。
回到這個問題自己,小程序自定義組件的 created、ready 和 error 三個函數與 document.ready 有殊途同歸之妙,都是結合框架自己特點以及業務需求所開發的超越標準規範以外的「創新」。
總的來講,以上兩個差別點的核心緣由能夠歸納爲一句話:理論上的規範在實現的時候須要結合現實的客觀條件。規範是上層實現的參考標準,但並無限制和框定上層實現的具體模式。差別點一是因爲小程序不存在操做 DOM 的狀況,差別點二是因爲created、ready 和 error 三個函數是超出規範以外、小程序根據自身技術特點的一種「創新」。 理解了自定義組件的資源管理和生命週期以後,你即可以開發出一個優秀的自定義組件了。可是正如上文提到的,一個 Page 中可能存在多個自定義組件,這些組件都是服務於同一個頁面,不免會有一些數據上的流通。這時候就會遇到一個組件化領域很是典型的問題:各組件之間如何通訊?
// toast.wxml
<view class="container {{mask?'containerShowMask':'containerNoMask'}}" hidden="{{!status}}" style="z-index:{{zIndex}}">
<view class="loreal-bg-class toast-bg" wx:if="{{mask}}"></view>
<view class="loreal-class toast toast-{{placement || 'bottom'}}" style="padding-top:{{(placement || 'bottom')=== 'bottom' ? image || icon ? '25rpx': '': ''}};position:relative;left:{{offsetX}}rpx;top:{{offsetY}}rpx;margin-bottom:{{distance}}px"> <image class="loreal-image-class toast-icon" wx:if="{{image}}" src="{{image}}"/> <l-icon class="loreal-icon-class toast-icon toast-icon-{{icon === 'loading'?'loading':''}}" wx:elif="{{icon && !image}}" size="{{iconSize? iconSize : 60}}" color="{{iconColor? iconColor: icon === 'success'? '#00C292' : icon === 'error' ? '#F4516C' : '#ffffff'}}" name="{{icon}}"/> <slot wx:else/> <text class="toast-text loreal-title-class toast-text-{{placement}}" style="{{placement || 'bottom' === 'bottom' ? icon || image? 'margin-top:10rpx' : '': '' }}">{{ title }}</text> </view>
</view>
// toast.js
import validator from "../behaviors/validator";
Component({
externalClasses: ["loreal-class", "loreal-label-class", "loreal-hover-class", "loreal-img-class", "loreal-icon-class"],
behaviors: [validator],
properties: {
name: {
type: String,
value: "lin"
},
type: {
type: String,
value: "default",
options: ["warning", "success", "error", "default"]
},
plain: Boolean,
size: {
type: String,
value: "medium",
options: ["medium", "large", "mini", "long"]
},
shape: {
type: String,
value: "circle",
options: ["square", "circle", "semicircle"]
},
disabled: {
type: Boolean,
value: !1
},
special: {
type: Boolean,
value: !1
},
loading: {
type: Boolean,
value: !1
},
width: Number,
height: Number,
icon: String,
image: String,
bgColor: String,
iconColor: String,
iconSize: String,
openType: String,
appParameter: String,
lang: String,
hoverStopPropagation: Boolean,
hoverStartTime: {
type: Number,
value: 20
},
hoverStayTime: {
type: Number,
value: 70
},
sessionFrom: {
type: String,
value: ""
},
sendMessageTitle: String,
sendMessagePath: String,
sendMessageImg: String,
showMessageCard: Boolean,
formType: String
},
methods: {
handleTap() {
if (this.data.disabled || this.data.loading) return !1;
this.triggerEvent("lintap", {}, {
bubbles: !0,
composed: !0
})
},
openTypeEvent(e) {
this.triggerEvent(e.type, e.detail, {})
}
}
});
// toast.wxss
.container{position:fixed}.containerNoMask{left:50%;top:50%;transform:translate(-50%,-50%)}.containerShowMask{height:100%;width:100%;top:0;left:0;display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:999}.container .toast-bg{height:100%;width:100%;background:rgba(255,255,255,.5);position:absolute;top:0;left:0}.container .toast-top{flex-direction:column-reverse}.container .toast-right{flex-direction:row}.container .toast-bottom{flex-direction:column}.container .toast-left{flex-direction:row-reverse}.container .toast{display:flex;align-items:center;justify-content:center;max-width:400rpx;min-width:280rpx;min-height:88rpx;background:rgba(0,0,0,.7);border-radius:12rpx;color:#fff;font-size:28rpx;line-height:40rpx;box-sizing:border-box;padding:30rpx 50rpx;z-index:999}.container .toast .toast-icon{margin-top:20rpx;margin-bottom:20rpx}.container .toast .toast-icon-loading{animation:loading-fadein 1.5s linear 0s infinite}.container .toast .toast-text{display:inline-block;text-align:center}.container .toast .toast-text-right{display:inline-block;text-align:center;margin-left:20rpx}.container .toast .toast-text-left{display:inline-block;text-align:center;margin-right:20rpx}.container .toast .toast-text-top{margin-bottom:10rpx}@keyframes loading-fadein{0%{transform:rotate(0)}100%{transform:rotate(360deg)}}
複製代碼
除此以外,自定義組件還有一些特殊的生命週期,它們並不是與組件有很強的關聯,但有時組件須要獲知,以便組件內部處理。這樣的生命週期稱爲「組件所在頁面的生命週期」,在 pageLifetimes 定義段中定義。其中可用的生命週期包括:
生成的組件實例能夠在組件的方法、生命週期函數和屬性 observer 中經過 this 訪問。組件包含一些通用屬性和方法。
組件間交互的主要形式是自定義事件。組件經過 this.triggerEvent() 觸發自定義事件,主頁面在組件上 bind:myevent="onMyEvent" 來接收自定義事件。其中,this.triggerEvent() 方法接收自定義事件名稱外,還接收兩個對象,eventDetail 和 eventOptions。
<!-- 在自定義組件中 -->
<button bindtap="onTap">點擊這個按鈕將觸發「myevent」事件</button>
Component({
properties: {}
methods: {
// 子組件觸發自定義事件
ontap () {
// 全部要帶到主頁面的數據,都裝在eventDetail裏面
var eventDetail = {
name:'sssssssss',
test:[1,2,3]
}
// 觸發事件的選項 bubbles是否冒泡,composed是否可穿越組件邊界,capturePhase 是否有捕獲階段
var eventOption = {
composed: true
}
this.triggerEvent('myevent', eventDetail, eventOption)
}
}
})
複製代碼
觸發的事件包括:
自定義組件能夠觸發任意的事件,引用組件的頁面能夠監聽這些事件。監聽自定義組件事件的方法與監聽基礎組件事件的方法徹底一致:在Page事件中監聽組件中傳遞過來的值。
Page({
onMyEvent: function(e){
e.detail // 自定義組件觸發事件時提供的detail對象
}
})
複製代碼
behaviors 是用於組件間代碼共享的特性,相似於一些編程語言中的「mixins」或「traits」。每一個 behavior 能夠包含一組屬性、數據、生命週期函數和方法,組件引用它時,它的屬性、數據和方法會被合併到組件中,生命週期函數也會在對應時機被調用。每一個組件能夠引用多個 behavior 。behavior 也能夠引用其餘 behavior 。
// validator.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(){}
}
})
複製代碼
組件和它引用的 behavior 中能夠包含同名的字段,對這些字段的處理方法以下:若是有同名的屬性或方法,組件自己的屬性或方法會覆蓋 behavior 中的屬性或方法,若是引用了多個 behavior ,在定義段中靠後 behavior 中的屬性或方法會覆蓋靠前的屬性或方法;若是有同名的數據字段,若是數據是對象類型,會進行對象合併,若是是非對象類型則會進行相互覆蓋;生命週期函數不會相互覆蓋,而是在對應觸發時機被逐個調用。若是同一個 behavior 被一個組件屢次引用,它定義的生命週期函數只會被執行一次。內置behavior 組件間關係
<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.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生命週期以後
}
}
}
})
複製代碼
與 Vue/React 不一樣,小程序沒有相似 Vuex 或 Redux 數據流管理模塊,因此小程序的自定義組件之間的通訊流程採用的是比較原始的事件驅動模式,即子組件經過拋出事件將數據傳遞給父組件,父組件經過 properties 將數據傳遞給子組件。
假設小程序的某個頁面中存在兩個組件,兩個組件均依賴父組件(Page)的部分屬性,這部分屬性經過 properties 傳遞給子組件,以下圖所示:
當組件 A 須要與組件 B 進行通訊時,會拋出一個事件通知父組件 Page,父組件接收到事件以後提取事件攜帶的信息,而後經過 properties 傳遞給組件 B。這樣便完成了子組件之間的消息傳遞。
除了事件驅動的通訊方式之外,小程序還提供了一種更加簡單粗暴的方法:父組件經過selectComponent 方法直接獲取某個子組件的實例對象,而後就能夠訪問這個子組件的任何屬性和方法了。隨後將這個子組件的某個屬性經過 properties傳遞個另一個子組件。相較而言,事件驅動的方法更加優雅,在流程上也更加可控,因此一般建議使用事件驅動的通訊方式。