Vue組件間的通信方式詳析
前言
在Vue組件庫開發(fā)過程中,Vue組件之間的通信一直是一個重要的話題,雖然官方推出的 Vuex 狀態(tài)管理方案可以很好的解決組件之間的通信問題,但是在組件庫內(nèi)部使用 Vuex 往往會比較重,本文將系統(tǒng)的羅列出幾種不使用 Vuex,比較實用的組件間的通信方式,供大家參考。
組件之間通信的場景
在進(jìn)入我們今天的主題之前,我們先來總結(jié)下Vue組件之間通信的幾種場景,一般可以分為如下幾種場景:
- 父子組件之間的通信
- 兄弟組件之間的通信
- 隔代組件之間的通信
父子組件之間的通信
父子組件之間的通信應(yīng)該是 Vue 組件通信中最簡單也最常見的一種了,概括為兩個部分:父組件通過prop向子組件傳遞數(shù)據(jù),子組件通過自定義事件向父組件傳遞數(shù)據(jù)。
父組件通過 prop 向子組件傳遞數(shù)據(jù)
Vue組件的數(shù)據(jù)流向都遵循單向數(shù)據(jù)流的原則,所有的 prop 都使得其父子 prop 之間形成了一個單向下行綁定:父級 prop 的更新會向下流動到子組件中,但是反過來則不行。這樣會防止從子組件意外變更父級組件的狀態(tài),從而導(dǎo)致你的應(yīng)用的數(shù)據(jù)流向難以理解。
額外的,每次父級組件發(fā)生變更時,子組件中所有的 prop 都將會刷新為最新的值。這意味著你不應(yīng)該在一個子組件內(nèi)部改變 prop。如果你這樣做了,Vue 會在瀏覽器的控制臺中發(fā)出警告。
父組件 ComponentA:
<template> <div> <component-b title="welcome"></component-b> </div> </template> <script> import ComponentB from './ComponentB' export default { name: 'ComponentA', components: { ComponentB } } </script>
子組件 ComponentB:
<template> <div> <div>{{title}}</div> </div> </template> <script> export default { name: 'ComponentB', props: { title: { type: String, } } } </script>
子組件通過自定義事件向父組件傳遞數(shù)據(jù)
在子組件中可以通過 $emit
向父組件發(fā)生一個事件,在父組件中通過 v-on
/@
進(jìn)行監(jiān)聽。
子組件 ComponentA:
<template> <div> <component-b :title="title" @title-change="titleChange"></component-b> </div> </template> <script> import ComponentB from './ComponentB' export default { name: 'ComponentA', components: { ComponentB }, data: { title: 'Click me' }, methods: { titleChange(newTitle) { this.title = newTitle } } } </script>
子組件 ComponentB:
<template> <div> <div @click="handleClick">{{title}}</div> </div> </template> <script> export default { name: 'ComponentB', props: { title: { type: String, } }, methods: { handleClick() { this.$emit('title-change', 'New title !') } } } </script>
這個例子非常簡單,在子組件 ComponentB 里面通過 $emit
派發(fā)一個事件 title-change
,在父組件 ComponentA 通過 @title-change
綁定的 titleChange
事件進(jìn)行監(jiān)聽,ComponentB 向 ComponentA 傳遞的數(shù)據(jù)在 titleChange
函數(shù)的傳參中可以獲取到。
兄弟組件之間的通信
狀態(tài)提升
寫過 React 的同學(xué)應(yīng)該對組件的 狀態(tài)提升
概念并不陌生,React 里面將組件按照職責(zé)的不同劃分為兩類:展示型組件(Presentational Component)
和 容器型組件(Container Component)
。
展示型組件不關(guān)心組件使用的數(shù)據(jù)是如何獲取的,以及組件數(shù)據(jù)應(yīng)該如何修改,它只需要知道有了這些數(shù)據(jù)后,組件UI是什么樣子的即可。外部組件通過 props 傳遞給展示型組件所需的數(shù)據(jù)和修改這些數(shù)據(jù)的回調(diào)函數(shù),展示型組件只是它們的使用者。
容器型組件的職責(zé)是獲取數(shù)據(jù)以及這些數(shù)據(jù)的處理邏輯,并把數(shù)據(jù)和邏輯通過 props 提供給子組件使用。
因此,參考 React 組件中的 狀態(tài)提升
的概念,我們在兩個兄弟組件之上提供一個父組件,相當(dāng)于容器組件,負(fù)責(zé)處理數(shù)據(jù),兄弟組件通過 props 接收參數(shù)以及回調(diào)函數(shù),相當(dāng)于展示組件,來解決兄弟組件之間的通信問題。
ComponentA(兄弟組件A):
<template> <div> <div>{{title}}</div> <div @click="changeTitle">click me</div> </div> </template> <script> export default { name: 'ComponentA', props: { title: { type: String }, changeTitle: Function } } </script>
ComponentB(兄弟組件B):
<template> <div> <div>{{title}}</div> <div @click="changeTitle">click me</div> </div> </template> <script> export default { name: 'ComponentB', props: { title: { type: String }, changeTitle: Function } } </script>
ComponentC(容器組件C):
<template> <div> <component-a :title="titleA" :change-title="titleAChange"></component-a> <component-b :title="titleB" :change-title="titleBChange"></component-b> </div> </template> <script> import ComponentA from './ComponentA' import ComponentB from './ComponentB' export default { name: 'ComponentC', components: { ComponentA, ComponentB }, data: { titleA: 'this is title A', titleB: 'this is title B' }, methods: { titleAChange() { this.titleA = 'change title A' }, titleBChange() { this.titleB = 'change title B' } } } </script>
可以看到,上述這種 "狀態(tài)提升" 的方式是比較繁瑣的,特別是兄弟組件的通信還要借助于父組件,組件復(fù)雜之后處理起來是相當(dāng)麻煩的。
隔代組件之間的通信
隔代組件之間的通信可以通過如下幾種方式實現(xiàn):
$attrs
/$listeners
rovide
/inject
- 基于
$parent
/$children
實現(xiàn)的dispatch
和broadcast
attrs/attrs/listeners
Vue 2.4.0 版本新增了 $attrs
和 $listeners
兩個方法。先看下官方對 $attrs
的介紹:
包含了父作用域中不作為 prop 被識別 (且獲取) 的 attribute 綁定(
class
和style
除外)。當(dāng)一個組件沒有聲明任何 prop 時,這里會包含所有父作用域的綁定 (class
和style
除外),并且可以通過v-bind="$attrs"
傳入內(nèi)部組件——在創(chuàng)建高級別的組件時非常有用。
看個例子:
組件A(ComponentA):
<template> <component-a name="Lin" age="24" sex="male"></component-a> </template> <script> import ComponentB from '@/components/ComponentB.vue' export default { name: 'App', components: { ComponentA } } </script>
組件B(ComponetB):
<template> <div> I am component B <component-c v-bind="$attrs"></component-c> </div> </template> <script> import ComponentC from '@/components/ComponentC.vue' export default { name: 'ComponentB', inheritAttrs: false, components: { ComponentC } } </script>
組件C(ComponetC):
<template> <div> I am component C </div> </template> <script> export default { name: 'ComponentC', props: { name: { type: String } }, mounted: function() { console.log('$attrs', this.$attrs) } } </script>
這里有三個組件,祖先組件(ComponentA)、父組件(ComponentB)和子組件(ComponentC)。這三個組件構(gòu)成了一個典型的子孫組件之間的關(guān)系。
ComponetA 給 ComponetB 傳遞了三個屬性 name、age 和 sex,ComponentB 通過 v-bind="$attrs"
將這三個屬性再透傳給 ComponentC, 最后在 ComponentC 中打印 $attrs
的值為:
{age: '24', sex: 'male'}
為什么我們一開始傳遞了三個屬性,最后只打印了兩個屬性 age 和 sex 呢?因為在 ComponentC 的props 中聲明了 name 屬性,$attrs
會自動排除掉在 props 中聲明的屬性,并將其他屬性以對象的形式輸出。
說白了就是一句話,$attrs
可以獲取父組件中綁定的非 Props 屬性。
一般在使用的時候會同時和 inheritAttrs
屬性配合使用。
如果你不希望組件的根元素繼承 attribute,你可以在組件的選項中設(shè)置 inheritAttrs: false
。
在 ComponentB 添加了 inheritAttrs=false
屬性后,ComponentB 的dom結(jié)構(gòu)中可以看到是不會繼承父組件傳遞過來的屬性:
如果不加上 inheritAttrs=false
屬性,就會自動繼承父組件傳遞過來的屬性:
再看下 $listeners
的定義:
包含了父作用域中的 (不含
.native
修飾器的)v-on
事件監(jiān)聽器。它可以通過v-on="$listeners"
傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時非常有用。
$listeners
也能把父組件中對子組件的事件監(jiān)聽全部拿到,這樣我們就能用一個v-on
把這些來自于父組件的事件監(jiān)聽傳遞到下一級組件。
繼續(xù)改造 ComponentB 組件:
<template> <div> I am component B <component-c v-bind="$attrs" v-on="$listeners"></component-c> </div> </template> <script> import ComponentC from '@/components/ComponentC.vue' export default { name: 'ComponentB', inheritAttrs: false, components: { ComponentC } } </script>
這里利用 $attrs
和 $listeners
方法,可以將祖先組件(ComponentA) 中的屬性和事件透傳給孫組件(ComponentC),這樣就可以實現(xiàn)隔代組件之間的通信。
provide/inject
provide/inject
是 Vue 2.2.0 版本后新增的方法。
這對選項需要一起使用,以允許一個祖先組件向其所有子孫后代注入一個依賴,不論組件層次有多深,并在其上下游關(guān)系成立的時間里始終生效。如果你熟悉 React,這與 React 的上下文特性很相似。
先看下簡單的用法:
父級組件:
export default { provide: { name: 'Lin' } }
子組件:
export default { inject: ['name'], mounted () { console.log(this.name); // Lin } }
上面的例子可以看到,父組件通過 privide
返回的對象里面的值,在子組件中通過 inject
注入之后可以直接訪問到。
但是需要注意的是,provide
和 inject
綁定并不是可響應(yīng)的,按照官方的說法,這是刻意為之的。
也就是說父組件 provide 里面的name屬性值變化了,子組件中 this.name 獲取到的值不變。
如果想讓 provide 和 inject 變成可響應(yīng)的,有以下兩種方式:
- provide 祖先組件的實例,然后在子孫組件中注入依賴,這樣就可以在子孫組件中直接修改祖先組件的實例的屬性,不過這種方法有個缺點(diǎn)就是這個實例上掛載很多沒有必要的東西比如props,methods
- 使用 Vue 2.6 提供的 Vue.observable 方法優(yōu)化響應(yīng)式 provide
看一下第一種場景:
祖先組件組件(ComponentA):
export default { name: 'ComponentA', provide() { return { app: this } }, data() { return { appInfo: { title: '' } } }, methods: { fetchAppInfo() { this.appInfo = { title: 'Welcome to Vue world'} } } }
我們把整個 ComponentA.vue 的實例 this
對外提供,命名為 app
。接下來,任何組件只要通過 inject
注入 app 的話,都可以直接通過 this.app.xxx
來訪問 ComponentA.vue 的 data
、computed
、methods
等內(nèi)容。
子組件(ComponentB):
<template> <div> {{ title }} <button @click="fetchInfo">獲取App信息</button> </div> </template> <script> export default { name: 'ComponentB', inject: ['app'], computed: { title() { return this.app.appInfo.title } }, methods: { fetchInfo() { this.app.fetchAppInfo() } } } </script>
這樣,任何子組件,只要通過 inject
注入 app
后,就可以直接訪問祖先組件中的數(shù)據(jù)了,同時也可以調(diào)用祖先組件提供的方法修改祖先組件的數(shù)據(jù)并反應(yīng)到子組件上。
當(dāng)點(diǎn)擊子組件(ComponentB)的獲取App信息按鈕,會調(diào)用 this.app.fetchAppInfo
方法,也就是訪問祖先組件(ComponentA)實例上的 fetchAppInfo 方法,fetchAppInfo 會修改fetchAppInfo的值。同時子組件(ComponentB)中會監(jiān)聽 this.app.appInfo 的變化,并將變化后的title值顯示在組件上。
再看一下第二種場景,通過 Vue.observable
方法來實現(xiàn) provide
和 inject
綁定并可響應(yīng)。
基于上面的示例,改造祖先組件(ComponentA):
import Vue from 'vue' const state = Vue.observable({ title: '' }); export default { name: 'ComponentA', provide() { return { state } } }
使用 Vue.observable
定義一個可響應(yīng)的對象 state,并在 provide 中返回這個對象。
改造子組件(ComponentB):
<template> <div> {{ title }} <button @click="fetchInfo">獲取App信息</button> </div> </template> <script> export default { name: 'ComponentInject', inject: ['state'], computed: { title() { return this.state.title } }, methods: { fetchInfo() { this.state.title = 'Welcome to Vue world22' } } } </script>
與之前的例子不同的是,這里我們直接修改了 this.state.title 的值,因為 state 被定義成了一個可響應(yīng)的數(shù)據(jù),所以 state.title 的值被修改后,視圖上的 title 也會立即響應(yīng)并更新,從這里看,其實很像 Vuex
的處理方式。
以上兩種方式對比可以發(fā)現(xiàn),第二種借助于 Vue.observable
方法實現(xiàn) provide
和 inject
的可響應(yīng)更加簡單高效,推薦大家使用這種方式。
基于 $parent/$children 實現(xiàn)的 dispatch 和 broadcast
先了解下 dispatch 和 broadcast 兩個概念:
- dispatch: 派發(fā),指的是從一個組件內(nèi)部向上傳遞一個事件,并在組件內(nèi)部通過
$on
進(jìn)行監(jiān)聽 - broadcast: 廣播,指的是從一個組件內(nèi)部向下傳遞一個事件,并在組件內(nèi)部通過
$on
進(jìn)行監(jiān)聽
在實現(xiàn) dispatch 和 broadcast 方法之前,先來看一下具體的使用方法。有 ComponentA.vue 和 ComponentB.vue 兩個組件,其中 ComponentB 是 ComponentA 的子組件,中間可能跨多級,在 ComponentA 中向 ComponentB 通信:
組件ComponentA:
<template> <button @click="handleClick">派發(fā)事件</button> </template> <script> import Emitter from '../mixins/emitter.js'; export default { name: 'ComponentA', mixins: [Emitter], methods: { handleClick () { this.dispatch('ComponentB', 'on-message', 'Hello Vue.js') } } } </script>
組件ComponentB:
export default { name: 'ComponentB', created () { this.$on('on-message', this.showMessage) }, methods: { showMessage (text) { console.log(text) } } }
dispatch 的邏輯寫在 emitter.js
中,使用的時候通過 mixins
混入到組件中,這樣可以很好的將事件通信邏輯和組件進(jìn)行解耦。
dispatch 的方法有三個傳參,分別是:需要接受事件的組件的名字(全局唯一,用來精確查找組件)、事件名和事件傳遞的參數(shù)。
dispatch 的實現(xiàn)思路非常簡單,通過 $parent
獲取當(dāng)前父組件對象,如果組件的name和接受事件的name一致(dispatch方法的第一個參數(shù)),在父組件上調(diào)用 $emit
發(fā)射一個事件,這樣就會觸發(fā)目標(biāo)組件上 $on
定義的回調(diào)函數(shù),如果當(dāng)前組件的name和接受事件的name不一致,就遞歸地向上調(diào)用此邏輯。
dispath:
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邏輯和dispatch的邏輯差不多,只是一個是通過 $parent
向上查找,一個是通過 $children
向下查找,
export default { methods: { 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])) } }) } } }
到此這篇關(guān)于Vue組件間的通信方式詳析的文章就介紹到這了,更多相關(guān)Vue組件通信內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
elementUI中input回車觸發(fā)頁面刷新問題與解決方法
這篇文章主要給大家介紹了關(guān)于elementUI中input回車觸發(fā)頁面刷新問題與解決方法,文中通過實例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用elementUI具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2023-07-07element-ui中el-form-item內(nèi)的el-select該如何自適應(yīng)寬度
自從用了element-ui,確實好用,該有的組件都有,但是組件間的樣式都固定好了,下面這篇文章主要給大家介紹了關(guān)于element-ui中el-form-item內(nèi)的el-select該如何自適應(yīng)寬度的相關(guān)資料,需要的朋友可以參考下2022-11-11詳解vuex中mutations方法的使用與實現(xiàn)
這篇文章主要為大家詳細(xì)介紹了vuex中mutations方法的使用與實現(xiàn)的相關(guān)知識,文中的示例代碼簡潔易懂,具有一定的學(xué)習(xí)價值,感興趣的小伙伴可以跟隨小編一起了解一下2023-11-11vue中數(shù)據(jù)不響應(yīng)的問題及解決
這篇文章主要介紹了vue中數(shù)據(jù)不響應(yīng)的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-09-09element中TimePicker時間選擇器禁用部分時間(顯示禁用到分鐘)
這篇文章主要介紹了element中TimePicker時間選擇器禁用部分時間(顯示禁用到分鐘),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03把vue-router和express項目部署到服務(wù)器的方法
下面小編就為大家分享一篇把vue-router和express項目部署到服務(wù)器的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-02-02