組件可謂是 Vue框架的最有特點之一, 能夠將一大塊拆分爲小零件最後組裝起來。這樣的好處易於維護、擴展和複用等。javascript
提到 Vue的組件, 相必你們對Vue組件之間的數據流並不陌生。最常規的是父組件的數據經過 prop傳遞給子組件。子組件要進行數據更新,那麼須要經過自定義事件通知父組件。vue
那麼還有其餘方法, 方便父子組件乃至跨組件之間的通信嗎?java
能夠經過 prop屬性從父組件向子組件傳遞數據webpack
// child.vue Vue.component('child', { props: ['msg'], template: `<div>{{ msg }}</div>` }) // parent.vue <div> <child :msg="message"></child> <div> // 部分省略... { data(){ return { message: 'hello.child' } } }
以上代碼父子組件通信是經過 prop的傳遞, Vue是單向數據流, 子組件不能直接修改父組件的數據。能夠經過自定義事件通知父組件修改,和要修改的內容ios
provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。可是咱們若是合理的運用, 那仍是很是方便的web
provide
應該是一個對象, 或者是一個函數返回一個對象
。它主要的做用就是給應用provide
的組件的子孫組件提供注入數據。就至關於 Vuex的store
,用來存放數據。vue-router
inject
: 給當前的組件注入 provide
提供的數據。切記 provide
和 inject
綁定並非可響應的。vuex
看一下這兩個怎麼使用axios
a組件給子組件b提供數據數組
// a.vue export default { provide: { name: 'qiqingfu' } } // b.vue export default { inject: ['name'], mounted(){ console.log(this.name) // qiqingfu } }
以上代碼父組件a提供了 name: qiqingfu
的數據, 子組件和子孫組件若是須要就經過 inject
注入進來就可使用了
provide / inject 替代 Vuex存儲用戶的登陸數據實例
這裏並非必定要替代 vuex, 介紹另外一種可行性
咱們在使用 webpack進行構建 Vue項目時, 都會有一個入口文件 main.js
, 裏面一般都導入一些第三方庫如 vuex
、element
、vue-router
等。可是咱們也引入了一個 Vue的根組件 App.vue
。簡單的app.vue是這樣子的
<template> <div> <router-view></router-view> </div> </template> <script> export default { } </script>
這裏的 App.vue
是一個最外層的組件, 能夠用來存儲一些全局的數據和狀態。由於組件的解析都是以App.vue
做爲入口。引入你項目中全部的組件,它們的共同根組件就是App.vue
。因此咱們能夠在 App.vue
中將本身暴露出去
<template> <div> <router-view></router-view> </div> </template> <script> export default { provide () { return { app: this } } } </script>
以上代碼把整個 app.vue
實例的 this對外提供, 而且命令爲 app
。那麼子組件使用時只須要訪問或者調用 this.app.xxx
或者訪問 app.vue的 data
、computed
等。
由於 App.vue
只會渲染一次, 很適合作一些全局的狀態數據管理, 好比用戶的登陸信息保存在 App.vue
的 data
中。
app.vue
<template> <div> <router-view></router-view> </div> </template> <script> export default { provide () { return { app: this } }, data: { userInfo: null }, mounted() { this.getUserInfo() }, methods: { async getUserInfo() { const result = await axios.get('xxxxxx') if (result.code === 200) { this.userInfo = result.data.userinfo } } } } </script>
以上代碼,經過接口獲取用戶對數據信息, 保存在 App.vue的data中。並且provide
將實例提供給任何子組件使用。因此任何頁面和子組件均可以經過 inject
注入 app
。而且經過 this.app.userInfo
來取得用戶信息。
那麼若是用戶要修改當前的信息怎麼辦? App.vue只初始化一次呀?
// header.vue <template> <div> <p>用戶名: {{ app.userInfo.username }}</p> </div> <template> <script> export default { name: 'userHeader', inject: ['app'], methods: { async updateUserName() { const result = await axios.post(xxxxx, data) if (result === 200) { this.app.getUserInfo() } } } } </script>
以上代碼, 在header.vue
組件中用戶修改了我的信息, 只須要修改完成以後再調用一次 App.vue
根組件的 getUserInfo
方法, 就又會同步最新的修改數據。
父子組件通信的方法。能夠支持:
先聊一下 Vue實例的方法 $emit()
和$on()
。
$emit: 觸發當前實例上的事件。附加參數都會傳給監聽器回調。
$on: 監聽當前實例上的事件
也就是一個組件向父組件經過 $emit
自定義事件發送數據的時候, 它會被本身的 $on
方法監聽到
// child.vue export default { name: 'child', methods: { handleEvent() { this.$emit('test', 'hello, 這是child.vue組件') } }, mounted() { // 監聽自定義事件 this.$on('test', data => { console.log(data) // hello, 這是child.vue組件 }) } } // parent <template> <div> <child v-on:test="handleTest"></child> </div> <template> <script> export default { methods: { handleTest(event) { console.log(event) // hello, 這是child.vue組件 } } } </script>
以上代碼, $on
監聽本身觸發的$emit
事件, 由於不知道什麼時候會觸發, 因此會在組件的 created
和 mounted
鉤子中監聽。
看起來畫蛇添足, 沒有必要在本身組件中監聽本身調用的 $emit
。 可是若是當前組件的 $emit
在別的組件被調用, 而且傳遞數據的話那就不同了。
舉個例子
// child.vue export default { name: 'iChild', methods: { sayHello() { // 若是我在子組件中調用父組件實例的 $emit this.$parent.$emit('test', '這是子組件的數據') } } } // parent.vue export default = { name: 'iParent', mounted() { this.$on('test', data => { console.log(data) // 這是子組件的數據 }) } }
以上代碼, 在子組件調用父組件實例的 $emit
方法, 而且傳遞響應的數據。那麼在父組件的 mounted
鉤子中能夠用 $on
監聽事件。
若是這樣寫確定不靠譜, 因此咱們要封裝起來。哪一個子組件須要給父組件傳遞數據就將這個方法混入(mixins)到子組件
// emitter.js export default { methods: { dispatch(componentName, eventName, params) { let parent = context.$parent || context.$root let name = parent.$options.name while(parent && (!name || name !== componentNamee)) { parent = parent.$parent if (parent) { name = parent.$options.name } } if (parent) { parent.call.$emit(parent, eventName, params) } } } }
以上代碼對 dispatch
進行封裝, 三個參數分別是 接受數據的父組件name
、自定義事件名稱
、傳遞的數據
。
對上面的例子從新修改
import Emitter from './emitter' // child.vue export default { name: 'iChild', mixins: [ Emitter ], // 將方法混入到當前組件 methods: { sayHello() { // 若是我在子組件中調用父組件實例的 $emit this.dispatch('iParent', 'test', 'hello,我是child組件數據') } } } // parent.vue export default = { name: 'iParent', mounted() { this.$on('test', data => { console.log(data) // hello,我是child組件數據 }) } }
以上代碼, 子組件要向父組件傳遞數據。能夠將先 Emitter混入。而後再調用 dispatch
方法。第一個參數是接受數據的父組件, 也能夠是爺爺組件, 只要 name
值給的正確就能夠。而後接受數據的組件須要經過 $on
來獲取數據。
廣播是父組件向全部子孫組件傳遞數據, 須要在父組件中注入這個方法。實現以下:
// emitter.js export default { methods: { broadcast(componentName, eventName, params) { this.$children.forEach(child => { let name = child.$options.name if (name === componentName) { child.$emit.apply(child, [eventName].concat(params)) } else { // 遞歸 broadcast.apply(child, [componentName, eventName].concat([params])) } }) } } }
以上代碼是經過遞歸匹配子組件的 name
, 若是沒有找到那麼就遞歸尋找。找到以後調用子組件實例的 $emit()
方法而且傳遞數據。
使用:
// 兒子組件 child.vue export default { name: 'iChild', mounted() { this.$on('test', data => { console.log(data) }) } } // 父親組件 parent.vue <template> <div> <child></child> </div> <template> export default { name: 'iParent', components: { child } } // 爺爺組件 root.vue <template> <div> <parent></parent> </div> <template> import Emitter from './emitter' export default { name: 'iRoot', mixin: [ Emitter ], components: { parent }, methods: { this.broadcast('iChild', 'test', '爺爺組件給孫子傳數據') } }
以上代碼, root根組件給孫組件(child.vue)經過調用 this.broadcast
找到對應name的孫組件實例。child.vue只須要監聽 test
事件就能夠獲取數據。
這些方法並不是 Vue組件內置, 而是經過自行封裝, 最終返回你要找的組件的實例,進而能夠調用組件的方法和函數等。
使用場景:
findComponentUpwarp(context, componentName)
export function findComponentUpwarp(context, componentName) { let parent = context.$parent let name = parent.$options.name while(parent && (!name || name !== componentName)) { parent = parent.$parent if (parent) { name = parent.$options.name } } return parent }
這個函數接受兩個參數,分別是當前組件的上下文(this),第二個參數是向上查找的組件 name。最後返回找到組件的實例。
findComponentsUpwarp(context, componentName)
return Array
export function findComponentsUpwarp(context, componentName) { let parent = context.$parent let result = [] if (parent) { if (parent.$options.name === componentName) result.push(parent) return result.concat(findComponentsUpwarp(parent, componentName)) } else { return [] } }
這個函數接受兩個參數,分別是當前組件的上下文(this),第二個參數是向上查找的組件 name。最後返回一個全部組件實例的數組
findComponentDownwarp(context, componentName)
export function findComponentDownwarp(context, componentName) { let resultChild = null context.$children.forEach(child => { if (child.name === componentName) { resultChild = child break } else { resultChild = findComponentDownwarp(child, componentName) if (resultChild) break } }) return resultChild }
以上代碼接受兩個參數, 當前組件的上下文(this)和向下查找的組件name。返回第一個name和componentName相同的組件實例
findComponentsDownwarp(context, componentName)
export function findComponentsDownwarp(context, componentName) { return context.$children.reduce((resultChilds, child) => { if (child.$options.name === componentName) resultChilds.push(child) // 遞歸迭代 let foundChilds = findComponentsDownwarp(child, componentName) return resultChilds.concat(foundChilds) }, []) }
以上代碼接受兩個參數, 當前組件的上下文(this)和向下查找的組件name。返回一個全部組件實例的數組
findBrothersComponents(context, componentName, exceptMe?)
exceptMe默認爲true, 排除它本身, 若是設置爲false則包括當前組件
export function findBrothersComponents(context, componentName, exceptMe = true) { let res = context.$parent.$children.filter(child => child.$options.name === componentName) // 根據惟一表示_uid找到當前組件的下標 let index = res.findIndex(item => item._uid === context._uid) if (exceptMe) res.splice(index, 1) return res }
以上代碼經過第一個參數的上下文, 拿到它父組件中的全部子元素,最後根據 exceptMe
決定是否排除本身。