Vue的組件是其很是重要的系統,組件之間的通訊也是開發中不可避免的需求vue
通常來講Vue組件是如下幾種關係ajax
A組件和B組件、B組件和C組件、B組件和D組件是父子關係,C組件和D組件是兄弟關係,A組件和C/D組件是隔代關係。vuex
本文闡述了幾種經常使用的通訊方式和使用場景api
父組件經過 props
傳遞數據給子組件,子組件經過 emit
發送事件傳遞數據給父組件數組
這種父子通訊方式也就是典型的單向數據流,父組件經過 props
傳遞數據,子組件不能直接修改 props
, 而是必須經過發送事件的方式告知父組件修改數據。app
// component-a <template> <div id="app"> <com-b title="cc" @changeTitle="handChange"></com-b> <!-- 傳入title 接受自定義事件changeTitle --> </div> </template> <script> import comB from './components/comB.vue' export default { name: 'app', components: { comB }, methods:{ // 接受子組件傳遞的事件 handChange(val){ alert(val) } } } </script> // component-b <template> <div> {{title}} <button @click="handChange">Change</button> </div> </template> <script> export default { name: "com-b", props: ["title"],// props 父組件傳來的值 methods:{ // 觸發自定義事件,想父組件傳遞發送修改後的數據 handChange(){ this.$emit('changeTitle','newTitle') } } }; </script>
優勢:易於使用,結構清晰ide
缺點:只能用於父子組件通訊函數
這兩種都是直接獲得組件實例,使⽤後能夠直接調⽤組件的⽅法或訪問數據工具
// component-a <template> <div id="app"> <!-- 爲子組件註冊引用信息 --> <com-b ref="comB"></com-b> </div> </template> <script> import comB from "./components/comB.vue"; export default { name: "app", components: { comB }, mounted() { // 經過$refs獲取到對應子組件的引用信息 console.log(this.$refs.comB); } }; </script>
$parent
和$children
都是基於當前上下文訪問父組件和子組件ui
// component-a <template> <div id="app"> <com-b></com-b> </div> </template> <script> import comB from "./components/comB.vue"; export default { name: "app", components: { comB }, mounted() { // 經過$children獲取所有子組件 console.log(this.$children); } }; </script> // component-b <template> <div> </div> </template> <script> export default { name: "com-b", mounted(){ // 經過$parent獲取父組件 console.log(this.$parent); } }; </script>
ref
和$parent/$children
的優缺點和props&&emit
相同,弊端都是沒法在跨級和兄弟間通訊
ref
和$parent/$children
在跨級通訊中有必定的弊端。Vue.js 2.2.0
版本後新增 provide / inject
API
vue文檔
這對選項須要⼀起使⽤,以容許⼀個祖先組件向其全部⼦孫後代注⼊⼀個依賴,不論組件層次有多深,並在起上下游關係成⽴的時間⾥始終⽣效
provide 選項應該是一個對象或返回一個對象的函數。該對象包含可注入其子孫的屬性。
inject 選項應該是:
一個對象,對象的 key 是本地的綁定名,value 是:
一個對象,該對象的:
provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。
// 父級組件提供 'foo' var Provider = { provide: { foo: 'bar' }, // ... } // 子組件注入 'foo' var Child = { inject: ['foo'], created () { console.log(this.foo) // => "bar" } // ... }
在作 Vue ⼤型項⽬時,可使⽤ Vuex 作狀態管理。使用 provide / inject
,能夠模擬 達到 Vuex
的效果 。
使⽤ Vuex,最主要的⽬的是跨組件通訊、全局數據維護。⽐如⽤戶的登陸信息維護、通知信息維護等全局的狀態和數據
一般vue應用都有一個根根組件app.vue
,能夠⽤來存儲全部須要的全局數據和狀態,methods
等。項目中全部的組件其父組件都是app
,經過provide
將app
實例暴露對外提供
<template> <div> <router-view></router-view> </div> </template> <script> export default { provide () { return { app: this } } } </script>
接下來任何組件只要經過 inject
注⼊ app.vue
的 app
的話,均可以直接經過this.app.xxx
來訪問 app.vue
的 data、computed、methods
等內容
例如經過這個特性保存登陸信息
export default { provide() { return { app: this }; }, data() { return { userInfo: null }; }, methods: { getUserInfo() { // 這⾥經過 ajax 獲取⽤戶信息後,賦值給this.userInfo; $.ajax("/user", data => { this.userInfo = data; }); } }, mounted() { this.getUserInfo(); } };
以後在任何⻚⾯或組件,只要經過 inject
注⼊ app
後,就能夠直接訪問 userInfo
的數據了
<template> <div> {{ app.userInfo }} </div> </template> <script> export default { inject: ['app'] } </script>
優勢:
缺點:
Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。當須要開發開發大型單頁應用(SPA),就應該考慮使用Vuex了,它能把組件的共享狀態抽取出來,當作一個全局單例模式進行管理。這樣無論你在何處改變狀態,都會通知使用該狀態的組件作出相應修改。Vuex
官方文檔已經給出了詳細的使用方式
優勢:
缺點:
若是不是大型項目,狀態管理不復雜,數據量不是很大,沒有必要使用Vuex
可使用一個空的vue
實例做爲事件總線中間件Bus
處理組件間的通訊
首先在全局定義bus
let bus = new Vue(); var eventBus = { install(Vue, options) { Vue.prototype.$bus = bus; } }; Vue.use(eventBus);
而後就能夠在組件中使用$on
,$emit
,off
來監聽,分發和銷燬組件
分發組件
// component-c <template> <div> <button @click="handClick">handClick</button> </div> </template> <script> export default { name: "com-c", methods: { handClick: function() { this.$bus.$emit("bus", "val"); } } }; </script>
監聽組件
// component-d <template> <div></div> </template> <script> export default { name: "com-d", // ... created() { this.$bus.$on("bus", val => { //獲取傳遞的參數並進行操做 console.log(val); }); }, // 最好在組件銷燬前 // 清除事件監聽 beforeDestroy() { this.$bus.$off("evetn"); } }; </script>
最好在組件銷燬以前清除監聽事件
優勢:
缺點:
$dispatch
和 $broadcast
是Vue1.x中提供的API,前者⽤於向上級派發事件,只要是它的⽗級(⼀級或多級以上),均可以在組件內經過 $on 監聽到,後者相反,是由上級向下級⼴播事件
// 子組件 vm.$dispatch(eventName,params) // 父組件 vm.$on(eventName , (params) => { console.log(params); });
$broadcast
相似,只不過⽅向相反。這兩種⽅法⼀旦發出事件後,任何組件都是能夠接收到的,就近原則,⽽且會在第⼀次接收到後停⽌冒泡,除⾮返回 true
。
這2個方法在已經被棄用,Vue
官方給出的解釋是:
由於基於組件樹結構的事件流方式實在是讓人難以理解,而且在組件結構擴展的過程當中會變得愈來愈脆弱。這種事件方式確實不太好,咱們也不但願在之後讓開發者們太痛苦。而且$dispatch 和 $broadcast 也沒有解決兄弟組件間的通訊問題。
雖然在開發中,沒有Vuex這樣的專業狀態管理工具方便好用,可是在獨立組件庫和一些特殊場景中,也是很是好用的一種傳遞方式。
自行模擬dispatch/broadcast
沒法達到與原方法如出一轍的效果,可是基本功能都是能夠實現的,解決組件之間的通訊問題
方法有功能有向上/下找到對應的組件,觸發指定事件並傳遞數據,其下/上級組件已經經過$on
監聽了該事件。
首先須要正確地向上或向下找到對應的組件實例,並在它上⾯觸發⽅法。
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); } } };
這兩個⽅法都接收了三個參數,第⼀個是組件的 name 值,⽤於向上或向下遞歸遍從來尋找對應的組件,第⼆個和第三個就是上⽂分析的⾃定義事件名稱和要傳遞的數據。
在 dispatch
⾥,經過 while
語句,不斷向上遍歷更新當前組件(即上下⽂爲當前調⽤該⽅法的組件)的⽗組件實例(變量 parent
即爲⽗組件實例),直到匹配到定義的 componentName
與某個上級組件的 name
選項⼀致時,結束循環,並在找到的組件實例上,調⽤ $emit
⽅法來觸發⾃定義事件 eventName
。 broadcast
⽅法與之相似,只不過是向下遍歷尋找
優勢:
缺點:
上述介紹的各類通訊方法都有各自的侷限性,咱們能夠實現一個 findComponents
系列的方法,能夠實現
5個方法都是經過遞歸和遍歷,經過組件name
選項匹配到指定組件返回
function findComponentUpward(context, componentName) { let parent = context.$parent; // 獲取父級組件 let name = parent.$options.name; // 獲取父級組件名稱 // 若是父級存在 且 父級組件 沒有name 或 name與要尋找的組件名不一致,重置parent和name,再逐級尋找 while (parent && (!name || [componentName].indexOf(name) < 0)) { parent = parent.$parent; if (parent) name = parent.$options.name; } // 逐級查找父級組件名和傳參名是否一致,返回找到的parent return parent; }
findComponentUpward
接收兩個參數,第⼀個是當前上下⽂,即你要基於哪一個組件來向上尋找,⼀般都是基於當前的組件,也就是傳⼊ this;第⼆個參數是要找的組件的 name 。dispatch
是經過觸發和監聽事件來完成事件交互,findComponentUpward
會直接拿到組件的實例
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 []; } }
findComponentsUpward
會返回的是⼀個數組,包含了全部找到的組件實例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; }
context.$children
獲得的是當前組件的所有⼦組件,因此須要遍歷⼀遍,找到有沒有匹配到的組件 name
,若是沒找到,繼續遞歸找每一個 $children
的 $children
,直到找到最近的⼀個爲⽌
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); }, []); }
使⽤ reduce
作累加器,並⽤遞歸將找到的組件合併爲⼀個數組並返回
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; }
findBrothersComponents
多了⼀個參數 exceptMe
,是否把自己除外,默認是 true
。尋找兄弟組件的⽅法,是先獲取 context.$parent.$children
,也就是⽗組件的所有⼦組件,這⾥⾯當前包含了自己,全部也會有第三個參數exceptMe
。Vue.js
在渲染組件時,都會給每一個組件加⼀個內置的屬性 _uid
,這個 _uid
是不會重複的,藉此咱們能夠從⼀系列兄弟組件中把⾃⼰排除掉。