導語:組件之間的關系不外乎兩種, 父子組件和非父子組件,本文將對兩類組件之間的通信方式進行詳細闡述。
父子組件間的通信
通信方式1(單向綁定): Props down, Events up (建議使用)
Props down, Events up 是指 使用props向子組件傳遞數據,父組件屬性發生變化時,子組件可實時更新視圖;子組件發生變化,可以使用$emit發送事件消息,以此向父組件傳遞變化消息。
props 是單向的,當父組件的屬性變化時,將傳遞給子組件,但子組件中的props屬性變化不會影響父組件的屬性變化(props屬性類型是Object除外)。倘若使用vue1.0的.sync強制雙向,那么子組件的更改將會影響到父組件的狀態,隨着業務的增多,很容易讓數據流變得難以理解,最終陷入痛苦的泥潭。因此,vue2.0已經剔除.sync,且不允許在子組件中更改自身的props屬性。如果真的需要更改props,那一定是設計方案出了問題,請使用替代方案,如:在data選項或computed選項中再定義一個不同的變量進行數據轉換。這是props down。
既然父組件可以通過props像子組件傳遞信息了,那子組件的數據變化如何通知到父組件呢?
$emit的出現便解決了這一問題,該方法用於 子組件向父組件發送事件消息,可帶上子組件的數據信息一起發送,父組件接收到消息后,做出自身相應的變更。vue1.0 和vue2.0均支持$emit。這是events up。
如示例代碼1,父組件通過 age(props) 向子組件傳遞數據信息,子組件拿到后,通過$emit向父組件傳遞最新狀態。如果子組件涉及到可能會對age進行更改,則重新定義一個變量age$進行中轉。
【示例代碼1】
<body> <div id="app"> <p>parent age: {{age}}</p> <p><button @click="changeAge">changeAge to 333</button></p> <hr> <child :age="age" @on-age-change="onAgeChange"></child> </div> <script> Vue.component('child', { template: '<div><p>child age (props選項): {{age}}</p> child age$ (data選項): {{age}}</p> <button @click="changeAge(222)">changeAge to 222</button></div>', props: { age: Number }, data () { return { age$: this.age } }, methods: { changeAge (age) { this.age$ = age this.$emit('on-age-change', this.age$) } } }); new Vue({ el: '#app', data: { age: 111 }, methods: { onAgeChange (val) { this.age = val }, changeAge () { this.age = 333 } } }) </script> </body>
通信方式2(雙向綁定): .sync 或 v-model(建議在簡單場景中使用)
在復雜邏輯組件中,一定不要使用.sync,很容易在N個組件中繞暈。如圖, A 是 BC 的父組件,AB和AC都雙向了一個共同的props屬性(如:model.sync)。B中的Model變化除了影響父組件A,A的變化進而還會影響組件C,這時C就要爆炸了,這Model變化到底來自A,還是來自B。如此,Model的變化變得很難跟蹤,增大維護成本。如果B或C還watch model的話,啊呵,足以毀掉你一天的心情了。
父子組件直接雙向綁定是個隱式毒蟲,但對於某些基礎組件來說卻是只有益的蜜蜂,可以省掉不少麻煩。一些簡單基礎的組件,或不需要關心數據流的地方 使用.sync 或 v-model就會是代碼顯得簡潔,且一目了然。
示例代碼2 和 示例代碼3 效果圖:
vue1.0修飾符.sync可以強制props屬性雙向綁定,如示例代碼2,checked為雙向綁定,可以輕松完成radio單選組件。
vue2.0中對prop屬性進行直接賦值更改會拋錯,但如果prop屬性類型為object時,僅僅添加或更改props屬性內部的屬性不會拋錯。由於此特性,vue2.0不支持.sync修飾符。
【示例代碼2】
<body> <div id="app"> <radio v-for="item in data" :value="item.value" :checked.sync="checked">{{item.text}}</radio> </div> <script> Vue.component('radio', { template: '<label><input type="radio" :value="value" v-model="checked"><slot></slot></label>', props: { value: {}, checked: {} } }); new Vue({ el: '#app', data: { checked: '', data: [ {text: '2G', value: '2G'}, {text: '3G', value: '3G'}, {text: '4G', value: '4G'} ] } }) </script> </body>
vue2.0雖然已經棄用.sync,但有語法糖v-model,其 實質 就是 props down, events up,只是因為v-model隱藏了數據流,因此喚其為雙向綁定。 父組件向下使用props隱式屬性value將數據傳遞數據給子組件,子組件使用$emit('input')向上將數據返回給父組件。但正如上文所說,對於基礎組件或不關心數據流的組件使用雙向綁定是糟粕中的小蜂蜜,書寫簡潔,清晰明了。
【示例代碼3】
<body> <div id="app"> <radio v-for="item in data" :label="item.value" v-model="checked">{{item.text}}</radio> </div> <script> Vue.component('radio', { template: '<label><input type="radio" :value="label" v-model="checked"><slot></slot></label>', props: { label: {}, value: {} }, computed: { checked: { get () { return this.value }, set (val) { this.$emit('input', val) } } } }); new Vue({ el: '#app', data: { checked: '', data: [ {text: '2G', value: '2G'}, {text: '3G', value: '3G'}, {text: '4G', value: '4G'} ] } }) </script> </body>
拆解語法糖v-model,如示例代碼4。已熟知的朋友可以略過。
代碼第4行 <child :value="info" @input="dataChange"> </child>更為 <child v-model="info"></child> 效果一樣。
【示例代碼4】
<body> <div id="app"> <p>parent info: {{info}}</p> <child :value="info" @input="dataChange"></child> <p><button @click="changeInfo">changeInfo from parent</button></p> </div> <script> Vue.component('child', { template: '<div><p>child data: {{data}}</p> <button @click="changeData">changeData from child</button></div>', props: { value: {} }, computed: { data: { get () { return this.value }, set (val) { this.$emit('input', val) } } }, methods: { changeData () { this.data = 'This is a child component!' } } }); new Vue({ el: '#app', data: { info: 'This is a original component!' }, methods: { dataChange (info) { this.info = info }, changeInfo () { this.info = 'This is a parent component!' } } }) </script> </body>
通信方式3: $broadcast 和 $dispatch(不建議使用)
只有vue1.0中才有這兩種方法。
$dispatch首先會觸發自身實例,冒泡向上,直到某個父組件收到$dispatch消息。如果子組件內部使用了$dispatch,那么該組件的 父組件鏈在寫監聽事件時都必須格外小心,必須得有父組件截獲該消息,否則會一直冒泡。這是一項非常危險的行為,因為,父組件鏈中的組件 很難關注到所有子組件的dispatch消息,隨着$dispatch在組件中增多,頂層組件或中間組件想知道消息來自哪個子組件變得異常艱辛,事件流跟蹤困難,痛苦深淵由此開啟。
$broadcast會向每一個子樹路徑發送消息,一條路徑某個組件接收消息,則該路徑停止向下發送消息,其它路徑規則一樣。同理$dispatch,隨着通信的增加,消息的增多,子組件也將很難跟蹤監聽的消息到底來自哪個父組件。不注意的話,最后上演一場的 尋親記,想來也是持久精彩的。
$dispatch 和 $broadcast 都已在vue2.0中被棄用,如果實在想堅持使用,也可通過$emit進行模擬,通過apply或call改變this,遞歸便利。
非父子組件間的通信
1. 狀態管理方案 vuex(建議使用)
適合復雜數據流管理。詳細使用方法見如下站點,
2. 中央數據總線(建議在簡單的場景中使用)
創建一個單獨的空Vue實例(Events = new Vue()),作為事件收發器,在該實例上進行$emit, $on, $off等操作。適用場景:a. 使用Events.$on的組件不關心事件具體來源; b. 事件處理程序 執行與否 或 重復執行 都沒有副作用(如刷新、查詢等操作)。如示例代碼5,
【示例代碼5】
<body> <div id="app"> <h5>A組件</h5> <a-component></a-component> <h5>B組件</h5> <b-component></b-component> </div> <script> var Events = new Vue() Vue.component('AComponent', { template: '<button @click="changeBName">change B name</button>', methods: { changeBName () { Events.$emit('on-name-change', 'The name is from A component!') } } }); Vue.component('BComponent', { template: '<p>B name: {{name}}</p>', data () { return { name: 'sheep' } }, created () { Events.$on('on-name-change', (name) => { this.name = name }) } }); new Vue({ el: '#app' }) </script> </body>
為什么說只適用簡單場景呢? vue組件化的開發模式意味着一個組件很可能被多次實例化。請看下文分析,
假設 A組件使用Events.$emit('event'), 在同一個界面被實例化了兩次,現在的需求是,組件A實例1觸發消息'event'時,組件B根據消息'event'相應更新,組件A實例2觸發消息'event',組件B不能根據消息'event'進行相應的更新。這時,因為組件B使用的是Events.$on('event')就搞不清是由A組件實例1觸發的消息'event', 還是A組件實例2觸發的。
因此,使用該方法時,最好保證組件在同一界面只會被渲染一次,或這不需要關心Events.$emit由哪個實例觸發。
3. 利用對象的值傳遞特性
適用場景:列表中,需要更改某條數據的某項信息。當一個變量向另一個變量復制引用類型的值時,復制的值實際上是一個指針,指向存儲在堆中的同一個對象。因此,改變其中一個變量就會影響另一個變量。 如,在一個表格列表中,如何在 N行X列 更改 N行Y列 的數據?
【示例代碼6】
<body> <div id="app"> <table> <tr> <th v-for="title in table.head">{{title}}</th> </tr> <tr v-for="item in table.data"> <td>{{item.name}}</td> <td><status :value="item.status"></status></td> <td><t-switch :item="item">切換</t-switch></td> </tr> </table> </div> <script> Vue.component('status', { template: '<span>{{value}}</span>', props: { value: {} } }); Vue.component('t-switch', { template: '<button @click="switchStatus">切換狀態</button>', props: { item: {} }, methods: { switchStatus () { this.item.status = this.item.status === '有效' ? '無效' : '有效' } } }); new Vue({ el: '#app', data: { table: { head: ['廣告名稱', '狀態', '操作'], data: [] } }, ready () { var timer = setTimeout(() => { this.table.data = [ {name: 'adName1', status: '無效'}, {name: 'adName2', status: '有效'}, {name: 'adName3', status: '無效'} ] clearTimeout(timer) }, 1000) } }) </script> </body>
4. Vue-rx
https://github.com/vuejs/vue-rx
https://github.com/Reactive-Extensions/RxJS
簡而言之,無論是vue1.0,還是vue2.0,為保證清晰的數據流 和 事件流,父子組件通信遵循“props down, events up”的原則。非父子組件根據不同場景選擇不同的方案,大部分情況依然建議使用vuex狀態管理方案。特別復雜的場景,建議使用vue-rx。
————————————————————
只有足夠努力,才能看起來毫不費力!