Vue組件通訊 組件可謂是 Vue框架的最有特色之一, 可以將一大塊拆分為小零件最後組裝起來。這樣的好處易於維護、擴展和復用等。 提到 Vue的組件, 相必大家對Vue組件之間的數據流並不陌生。最常規的是父組件的數據通過 prop傳遞給子組件。子組件要進行數據更新,那麼需要通過自定義事件通知父組件。 ...
Vue組件通訊
組件可謂是 Vue框架的最有特色之一, 可以將一大塊拆分為小零件最後組裝起來。這樣的好處易於維護、擴展和復用等。
提到 Vue的組件, 相必大家對Vue組件之間的數據流並不陌生。最常規的是父組件的數據通過 prop傳遞給子組件。子組件要進行數據更新,那麼需要通過自定義事件通知父組件。
那麼還有其他方法, 方便父子組件乃至跨組件之間的通訊嗎?
props $emit
可以通過 prop屬性從父組件向子組件傳遞數據
// 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是單向數據流, 子組件不能直接修改父組件的數據。可以通過自定義事件通知父組件修改,和要修改的內容
provide / inject
provide 和 inject 主要為高階插件/組件庫提供用例。並不推薦直接用於應用程式代碼中。但是我們如果合理的運用, 那還是非常方便的
provide
應該是一個對象, 或者是一個函數返回一個對象
。它主要的作用就是給應用provide
的組件的子孫組件提供註入數據。就相當於 Vuex的store
,用來存放數據。
inject
: 給當前的組件註入 provide
提供的數據。切記 provide
和 inject
綁定並不是可響應的。
看一下這兩個怎麼使用
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
方法, 就又會同步最新的修改數據。
dispatch / broadcast
父子組件通訊的方法。可以支持:
- 父 - 子 傳遞數據 (廣播)
- 子 - 父 傳遞數據 (派發)
先聊一下 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)到子組件
dispatch 封裝
// 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
來獲取數據。
broadcast
廣播是父組件向所有子孫組件傳遞數據, 需要在父組件中註入這個方法。實現如下:
// 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
決定是否排除自己。