父子關係便是組件 A 在它的模板中使用了組件 B,那麼組件 A 就是父組件,組件 B 就是子組件。vue
// 註冊一個子組件 Vue.component('child', { data: function(){ return { text: '我是father的子組件!' } }, template: '<span>{{ text }}</span>' }) // 註冊一個父組件 Vue.component('father', { template: '<div><child></child></div>' // 在模板中使用了child組件 })
兩個組件互不引用,則爲兄弟組件。git
Vue.component('brother1', { template: '<div>我是大哥</div>' }) Vue.component('brother2', { template: '<div>我是小弟</div>' })
使用組件的時候:github
<div id="app"> <brother1></brother1> <brother2></brother2> </div>
就是在父子關係中,中間跨了不少個層級vue-router
一個再複雜的組件,都是由三部分組成的:prop、event、slot,它們構成了 Vue.js 組件的 API。vuex
prop 定義了這個組件有哪些可配置的屬性,組件的核心功能也都是它來肯定的。寫通用組件時,props 最好用對象的寫法,這樣能夠針對每一個屬性設置類型、默認值或自定義校驗屬性的值,這點在組件開發中很重要,然而不少人卻忽視,直接使用 props 的數組用法,這樣的組件每每是不嚴謹的。數組
插槽 slot,它能夠分發組件的內容。和 HTML 元素同樣,咱們常常須要向一個組件傳遞內容,像這樣:app
<alert-box> Something bad happened. </alert-box>
可能會渲染出這樣的東西:iview
Error!Something bad happended.
幸虧,Vue 自定義的 <slot> 元素讓這變得很是簡單:ide
Vue.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> ` })
如你所見,咱們只要在須要的地方加入插槽就好了——就這麼簡單!函數
兩種寫法:
<template> <button @click="handleClick"> <slot></slot> </button> </template> <script> export default { methods: { handleClick (event) { this.$emit('on-click', event); } } } </script>
經過 $emit,就能夠觸發自定義的事件 on-click ,在父級經過 @on-click 來監聽:
<i-button @on-click="handleClick"></i-button>
因此上面的示例也能夠這樣寫:
<i-button @click.native="handleClick"></i-button>
若是不寫 .native 修飾符,那上面的 @click 就是自定義事件 click,而非原生事件 click,但咱們在組件內只觸發了 on-click 事件,而不是 click,因此直接寫 @click 會監聽不到。
Vue.js 內置的通訊手段通常有兩種:
用 ref 來訪問組件(部分代碼省略):
// component-a export default { data () { return { title: 'Vue.js' } }, methods: { sayHello () { window.alert('Hello'); } } }
<template> <component-a ref="comA"></component-a> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.title); // Vue.js comA.sayHello(); // 彈窗 } } </script>
$parent 和 $children 相似,也是基於當前上下文訪問父組件或所有子組件的。
這兩種方法的弊端是,沒法在跨級或兄弟間通訊,好比下面的結構:
// parent.vue <component-a></component-a> <component-b></component-b> <component-b></component-b>
咱們想在 component-a 中,訪問到引用它的頁面中(這裏就是 parent.vue)的兩個 component-b 組件,那這種狀況下,是暫時沒法實現的,後面會講解到方法。
一種無依賴的組件通訊方法:Vue.js 內置的 provide / inject 接口
provide / inject 是 Vue.js 2.2.0 版本後新增的 API,在文檔中這樣介紹 :
這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。若是你熟悉 React,這與 React 的上下文特性很類似。
provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。
假設有兩個組件: A.vue 和 B.vue,B 是 A 的子組件:
// A.vue export default { provide: { name: 'Aresn' } } // B.vue export default { inject: ['name'], mounted () { console.log(this.name); // Aresn } }
須要注意的是:
provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。
只要一個組件使用了 provide 向下提供數據,那其下全部的子組件均可以經過 inject 來注入,無論中間隔了多少代,並且能夠注入多個來自不一樣父級提供的數據。須要注意的是,一旦注入了某個數據,那這個組件中就不能再聲明 這個數據了,由於它已經被父級佔有。
provide / inject API 主要解決了跨級組件間的通訊問題,不過它的使用場景,主要是子組件獲取上級組件的狀態,跨級組件間創建了一種主動提供與依賴注入的關係。而後有兩種場景它不能很好的解決:
這種父子(含跨級)傳遞數據的通訊方式,Vue.js 並無提供原生的 API 來支持,下面介紹一種在父子組件間通訊的方法 dispatch 和 broadcast。
若是父組件A下面有子組件B,組件B下面有組件C,這時若是組件A想傳遞數據給組件C怎麼辦呢? Vue 2.4開始提供了$attrs和$listeners來解決這個問題,可以讓組件A之間傳遞消息給組件C。
Vue.component('C',{ template:` <div> <input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)"> </div> `, methods:{ passCData(val){ //觸發父組件A中的事件 this.$emit('getCData',val) } } }) Vue.component('B',{ data(){ return { mymessage:this.message } }, template:` <div> <input type="text" v-model="mymessage" @input="passData(mymessage)"> <!-- C組件中能直接觸發getCData的緣由在於 B組件調用C組件時 使用 v-on 綁定了$listeners 屬性 --> <!-- 經過v-bind 綁定$attrs屬性,C組件能夠直接獲取到A組件中傳遞下來的props(除了B組件中props聲明的) --> <C v-bind="$attrs" v-on="$listeners"></C> </div> `, props:['message'],//獲得父組件傳遞過來的數據 methods:{ passData(val){ //觸發父組件中的事件 this.$emit('getChildData',val) } } }) Vue.component('A',{ template:` <div> <p>this is parent compoent!</p> <B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"></B> </div> `, data(){ return { message:'hello', messagec:'hello c' //傳遞給c組件的數據 } }, methods:{ getChildData(val){ console.log('這是來自B組件的數據') }, //執行C子組件觸發的事件 getCData(val){ console.log("這是來自C組件的數據:"+val) } } }) var app=new Vue({ el:'#app', template:` <div> <A></A> </div> ` })
要實現的 dispatch 和 broadcast 方法,將具備如下功能:
在子組件調用 dispatch 方法,向上級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該上級組件已預先經過 $on 監聽了這個事件;
相反,在父組件調用 broadcast 方法,向下級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該下級組件已預先經過 $on 監聽了這個事件。
// 部分代碼省略 import Emitter from '../mixins/emitter.js' export default { mixins: [ Emitter ], methods: { handleDispatch () { this.dispatch(); // ① }, handleBroadcast () { this.broadcast(); // ② } } }
//emitter.js 的代碼: function broadcast(componentName, eventName, params) { this.$children.forEach(child => { const name = child.$options.name; if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)); } else { broadcast.apply(child, [componentName, eventName].concat([params])); } }); } export default { methods: { dispatch(componentName, eventName, params) { let parent = this.$parent || this.$root; let name = parent.$options.name; while (parent && (!name || name !== componentName)) { parent = parent.$parent; if (parent) { name = parent.$options.name; } } if (parent) { parent.$emit.apply(parent, [eventName].concat(params)); } }, broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params); } } };
由於是用做 mixins 導入,因此在 methods 裏定義的 dispatch 和 broadcast 方法會被混合到組件裏,天然就能夠用 this.dispatch 和 this.broadcast 來使用。
這兩個方法都接收了三個參數,第一個是組件的 name 值,用於向上或向下遞歸遍從來尋找對應的組件,第二個和第三個就是上文分析的自定義事件名稱和要傳遞的數據。
能夠看到,在 dispatch 裏,經過 while 語句,不斷向上遍歷更新當前組件(即上下文爲當前調用該方法的組件)的父組件實例(變量 parent 即爲父組件實例),直到匹配到定義的 componentName 與某個上級組件的 name 選項一致時,結束循環,並在找到的組件實例上,調用 $emit 方法來觸發自定義事件 eventName。broadcast 方法與之相似,只不過是向下遍歷尋找。
來看一下具體的使用方法。有 A.vue 和 B.vue 兩個組件,其中 B 是 A 的子組件,中間可能跨多級,在 A 中向 B 通訊:
<!-- A.vue --> <template> <button @click="handleClick">觸發事件</button> </template> <script> import Emitter from '../mixins/emitter.js'; export default { name: 'componentA', mixins: [ Emitter ], methods: { handleClick () { this.broadcast('componentB', 'on-message', 'Hello Vue.js'); } } } </script>
// B.vue export default { name: 'componentB', created () { this.$on('on-message', this.showMessage); }, methods: { showMessage (text) { window.alert(text); } } }
同理,若是是 B 向 A 通訊,在 B 中調用 dispatch 方法,在 A 中使用 $on 監聽事件便可。
以上就是自行實現的 dispatch 和 broadcast 方法。
它適用於如下場景:
5 個不一樣的場景,對應 5 個不一樣的函數,實現原理也大同小異。
// 由一個組件,向上找到最近的指定組件 function findComponentUpward (context, componentName) { let parent = context.$parent; let name = parent.$options.name; while (parent && (!name || [componentName].indexOf(name) < 0)) { parent = parent.$parent; if (parent) name = parent.$options.name; } return parent; } export { findComponentUpward };
好比下面的示例,有組件 A 和組件 B,A 是 B 的父組件,在 B 中獲取和調用 A 中的數據和方法:
<!-- component-a.vue --> <template> <div> 組件 A <component-b></component-b> </div> </template> <script> import componentB from './component-b.vue'; export default { name: 'componentA', components: { componentB }, data () { return { name: 'Aresn' } }, methods: { sayHello () { console.log('Hello, Vue.js'); } } } </script>
<!-- component-b.vue --> <template> <div> 組件 B </div> </template> <script> import { findComponentUpward } from '../utils/assist.js'; export default { name: 'componentB', mounted () { const comA = findComponentUpward(this, 'componentA'); if (comA) { console.log(comA.name); // Aresn comA.sayHello(); // Hello, Vue.js } } } </script>
// 由一個組件,向上找到全部的指定組件 function findComponentsUpward (context, componentName) { let parents = []; const parent = context.$parent; if (parent) { if (parent.$options.name === componentName) parents.push(parent); return parents.concat(findComponentsUpward(parent, componentName)); } else { return []; } } export { findComponentsUpward };
// 由一個組件,向下找到最近的指定組件 function findComponentDownward (context, componentName) { const childrens = context.$children; let children = null; if (childrens.length) { for (const child of childrens) { const name = child.$options.name; if (name === componentName) { children = child; break; } else { children = findComponentDownward(child, componentName); if (children) break; } } } return children; } export { findComponentDownward };
// 由一個組件,向下找到全部指定的組件 function findComponentsDownward (context, componentName) { return context.$children.reduce((components, child) => { if (child.$options.name === componentName) components.push(child); const foundChilds = findComponentsDownward(child, componentName); return components.concat(foundChilds); }, []); } export { findComponentsDownward };
// 由一個組件,找到指定組件的兄弟組件 function findBrothersComponents (context, componentName, exceptMe = true) { let res = context.$parent.$children.filter(item => { return item.$options.name === componentName; }); let index = res.findIndex(item => item._uid === context._uid); if (exceptMe) res.splice(index, 1); return res; } export { findBrothersComponents };
相比其它 4 個函數,findBrothersComponents 多了一個參數 exceptMe,是否把自己除外,默認是 true。尋找兄弟組件的方法,是先獲取 context.$parent.$children,也就是父組件的所有子組件,這裏面當前包含了自己,全部也會有第三個參數 exceptMe。Vue.js 在渲染組件時,都會給每一個組件加一個內置的屬性 _uid,這個 _uid 是不會重複的,藉此咱們能夠從一系列兄弟組件中把本身排除掉。
有時候兩個組件之間須要進行通訊,可是它們彼此不是父子組件的關係。在一些簡單場景,你可使用一個空的 Vue 實例做爲一個事件總線中心(central event bus):
//中央事件總線 var bus=new Vue(); var app=new Vue({ el:'#app', template:` <div> <brother1></brother1> <brother2></brother2> </div> ` }) // 在組件 brother1 的 methods 方法中觸發事件 bus.$emit('say-hello', 'world') // 在組件 brother2 的 created 鉤子函數中監聽事件 bus.$on('say-hello', function (arg) { console.log('hello ' + arg); // hello world })
若是業務邏輯複雜,不少組件之間須要同時處理一些公共的數據,這個時候纔有上面這一些方法可能不利於項目的維護,vuex的作法就是將這一些公共的數據抽離出來,而後其餘組件就能夠對這個公共數據進行讀寫操做,這樣達到了解耦的目的。 詳情可參考:https://vuex.vuejs.org/zh-cn/
參考 vue組件之間8種組件通訊方式總結
參考 https://github.com/iview/ivie...
參考 https://github.com/iview/ivie...