Vue 組件數據通訊方案總結
- 2019 年 10 月 7 日
- 筆記
背景
初識 Vue.js ,了解到組件是 Vue 的主要構成部分,但組件內部的作用域是相對獨立的部分,組件之間的關係一般如下圖:

組件 A 與組件 B 、C 之間是父子組件,組件 B 、C 之間是兄弟組件,而組件 A 、D 之間是隔代的關係。
那麼對於這些不同的關係,本文主要分享了他們之間可以採用的幾種數據通訊方式,例如 Props 、$emit / $on 、Vuex 等,大家可以根據自己的使用場景可以選擇適合的使用方式。
一、 Prop / $emit
1、Prop 是你可以在組件上註冊的一些自定義特性。當一個值傳遞給一個 Prop 特性的時候,它就變成了那個組件實例的一個屬性。父組件向子組件傳值,通過綁定屬性來向子組件傳入數據,子組件通過 Props 屬性獲取對應數據
// 父組件 <template> <div class="container"> <child :title="title"></child> </div> </template> <script> import Child from "./component/child.vue"; export default { name: "demo", data: function() { return { title: "我是父組件給的" }; }, components: { Child }, }; </script>
// 子組件 <template> <div class="text">{{title}}</div> </template> <script> export default { name: 'demo', data: function() {}, props: { title: { type: String } }, }; </script>
2、$emit 子組件向父組件傳值(通過事件形式),子組件通過 $emit 事件向父組件發送消息,將自己的數據傳遞給父組件。

// 父組件 <template> <div class="container"> <div class="title">{{title}}</div> <child @changeTitle="parentTitle"></child> </div> </template> <script> import Child from "./component/child.vue"; export default { name: "demo", data: function() { return { title: null }; }, components: { Child }, methods: { parentTitle(e) { this.title = e; } } }; </script>
// 子組件 <template> <div class="center"> <button @click="childTitle">我給父組件賦值</button> </div> </template> <script> export default { name: 'demo', data() { return { key: 1 }; }, methods: { childTitle() { this.$emit('changeTitle', `我給父組件的第${this.key}次`); this.key++; } } }; </script>
小總結:常用的數據傳輸方式,父子間傳遞。
二、 $emit / $on
這個方法是通過創建了一個空的 vue 實例,當做 $emit 事件的處理中心(事件匯流排),通過他來觸發以及監聽事件,方便的實現了任意組件間的通訊,包含父子,兄弟,隔代組件。

// 父組件 <template> <div class="container"> <child1 :Event="Event"></child1> <child2 :Event="Event"></child2> <child3 :Event="Event"></child3> </div> </template> <script> import Vue from "vue"; import Child1 from "./component/child1.vue"; import Child2 from "./component/child2.vue"; import Child3 from "./component/child3.vue"; const Event = new Vue(); export default { name: "demo", data: function() { return { Event: Event }; }, components: { Child1, Child2, Child3 }, }; </script>
// 子組件1 <template> <div class="center"> 1.我的名字是:{{name}} <button @click="send">我給3組件賦值</button> </div> </template> <script> export default { name: "demo1", data() { return { name: "政采雲" }; }, props: { Event }, methods: { send() { this.Event.$emit("message-a", this.name); } } }; </script>
// 子組件2 <template> <div class="center"> 2.我的年齡是:{{age}}歲 <button @click="send">我給3組件賦值</button> </div> </template> <script> /* eslint-disable */ export default { name: "demo2", data() { return { age: "3" }; }, props: { Event }, methods: { send() { this.Event.$emit("message-b", this.age); } } }; </script>
// 子組件3 <template> <div class="center">我的名字是{{name}},今年{{age}}歲</div> </template> <script> export default { name: 'demo3', data() { return { name: '', age: '' }; }, props: { Event }, mounted() { this.Event.$on('message-a', name => { this.name = name; }); this.Event.$on('message-b', age => { this.age = age; }); }, }; </script>
小總結:巧妙的在父子,兄弟,隔代組件中都可以互相數據通訊。
三、 Vuex
Vuex 是一個專為 Vue.js 應用程式開發的狀態管理模式。它採用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

Vuex實現了一個單項數據流,通過創建一個全局的 State 數據,組件想要修改 State 數據只能通過 Mutation 來進行,例如頁面上的操作想要修改 State 數據時,需要通過 Dispatch (觸發 Action ),而 Action 也不能直接操作數據,還需要通過 Mutation 來修改 State 中數據,最後根據 State 中數據的變化,來渲染頁面。

// index.js import Vue from 'vue'; import Tpl from './index.vue'; import store from './store'; new Vue({ store, render: h => h(Tpl), }).$mount('#app');
// store import Vue from 'vue'; import Vuex from 'vuex'; Vue.use(Vuex); const store = new Vuex.Store({ state: { count: 1 }, mutations: { increment(state) { state.count++; }, reduce(state) { state.count--; } }, actions: { actIncrement({ commit }) { commit('increment'); }, actReduce({ commit }) { commit('reduce'); } }, getters: { doubleCount: state => state.count*2 } }); export default store;
// vue文件 <template> <div class="container"> <p>我的count:{{count}}</p> <p>doubleCount:{{doubleCount}}</p> <button @click="this.actIncrement">增加</button> <button @click="this.actReduce">減少</button> </div> </template> <script> import { mapGetters, mapActions, mapState } from "vuex"; export default { name: "demo", data: function() { return {}; }, components: {}, props: {}, computed: { ...mapState(["count"]), ...mapGetters(["doubleCount"]) }, methods: { ...mapActions(["actIncrement", "actReduce"]) } }; </script>
Vuex 中需要注意的點:
Mutation :是修改State數據的唯一推薦方法,且只能進行同步操作。
Getter :Vuex 允許在Store中定義 "Getter"(類似於 Store 的計算屬性)。Getter 的返回值會根據他的依賴進行快取,只有依賴值發生了變化,才會重新計算。
本段只是簡單介紹了一下 Vuex 的運行方式,更多功能例如 Module 模組請參考官網 。
小總結:統一的維護了一份共同的 State 數據,方便組件間共同調用。
四、 $attrs / $listeners
Vue 組件間傳輸數據在 Vue2.4 版本後有了新方法。除了 Props 外,還有了 $attrs / $listeners。
- $attrs:包含了父作用域中不作為 Prop 被識別 (且獲取) 的特性綁定 (
Class
和Style
除外)。當一個組件沒有聲明任何 Prop 時,這裡會包含所有父作用域的綁定 (Class
和Style
除外),並且可以通過v-bind="$attrs"
傳入內部組件——在創建高級別的組件時非常有用。 - $listeners:包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部組件
下面來看個例子
// 父組件 <template> <div class="container"> <button style="backgroundColor:lightgray" @click="reduce">減dd</button> <child1 :aa="aa" :bb="bb" :cc="cc" :dd="dd" @reduce="reduce"></child1> </div> </template> <script> import Child1 from './component/child1.vue'; export default { name: 'demo', data: function() { return { aa: 1, bb: 2, cc: 3, dd: 100 }; }, components: { Child1 }, methods: { reduce() { this.dd--; } } }; </script>
// 子組件1 <template> <div> <div class="center"> <p>aa:{{aa}}</p> <p>child1的$attrs:{{$attrs}}</p> <button @click="this.reduce1">組件1減dd</button> </div> <child2 v-bind="$attrs" v-on="$listeners"></child2> </div> </template> <script> import child2 from './child2.vue'; export default { name: 'demo1', data() { return {}; }, props: { aa: Number }, components: { child2 }, methods: { reduce1() { this.$emit('reduce'); } } }; </script>
// 子組件2 <template> <div> <div class="center"> <p>bb:{{bb}}</p> <p>child2的$attrs:{{$attrs}}</p> <button @click="this.reduce2">組件2減dd</button> </div> <child3 v-bind="$attrs"></child3> </div> </template> <script> import child3 from './child3.vue'; export default { name: 'demo1', data() { return {}; }, props: { bb: Number }, components: { child3 }, methods: { reduce2() { this.$emit('reduce'); } } }; </script>
// 子組件3 <template> <div class="center"> <p>child3的$attrs:{{$attrs}}</p> </div> </template> <script> export default { name: 'demo3', data() { return {}; }, props: { dd: String }, }; </script>
簡單來說,$attrs 里存放的是父組件中綁定的非 props 屬性,$listeners 裡面存放的是父組件中綁定的非原生事件。
小總結:當傳輸數據、方法較多時,無需一一填寫的小技巧。
五、 Provider / Inject
Vue2.2 版本以後新增了這兩個 API ,**這對選項需要一起使用,以允許一個祖先組件向其所有子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裡始終生效。**簡單來說,就是父組件通過 Provider 傳入變數,任意子孫組件通過 Inject 來拿到變數。
// 父組件 <template> <div class="container"> <button @click="this.changeName">我要改名字了</button> <p>我的名字:{{name}}</p> <child1></child1> </div> </template> <script> import Child1 from './component/child1.vue'; export default { name: 'demo', data: function() { return { name: '政采雲' }; }, // provide() { // return { // name: this.name //這種綁定方式是不可響應的 // }; // }, provide() { return { obj: this }; }, components: { Child1 }, methods: { changeName() { this.name = '政采雲前端'; } } }; </script>
// 子組件 <template> <div> <div class="center"> <!-- <p>子組件名字:{{name}}</p> --> <p>子組件名字:{{this.obj.name}}</p> </div> <child2></child2> </div> </template> <script> import child2 from './child2.vue'; export default { name: 'demo1', data() { return {}; }, props: {}, // inject: ["name"], inject: { obj: { default: () => { return {}; } } }, components: { child2 }, }; </script>
需要注意的是:Provide 和 Inject 綁定並不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監聽的對象,那麼其對象的屬性還是可響應的。
所以,如果採用的是我程式碼中注釋的方式,父級的 name 如果改變了,子組件this.name 是不會改變的,仍然是 政采雲,而當採用程式碼中傳入一個監聽對象,修改對象中屬性值,是可以監聽到修改的。
Provider / Inject 在項目中需要有較多公共傳參時使用還是頗為方便的。
小總結:傳輸數據父級一次注入,子孫組件一起共享的方式。
六、 $parent / $children & $refs
- $parent / $children:指定已創建的實例之父實例,在兩者之間建立父子關係。子實例可以用
this.$parent
訪問父實例,子實例被推入父實例的$children
數組中。 - $refs:一個對象,持有註冊過
ref
特性 的所有 DOM 元素和組件實例。ref 被用來給元素或子組件註冊引用資訊。引用資訊將會註冊在父組件的$refs
對象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子組件上,引用就指向組件。
// 父組件 <template> <div class="container"> <p>我的title:{{title}}</p> <p>我的name:{{name}}</p> <child1 ref="comp1"></child1> <child2 ref="comp2"></child2> </div> </template> <script> import Child1 from './component/child1.vue'; import Child2 from './component/child2.vue'; export default { name: 'demo', data: function() { return { title: null, name: null, content: '就是我' }; }, components: { Child1, Child2 }, mounted() { const comp1 = this.$refs.comp1; this.title = comp1.title; comp1.sayHello(); this.name = this.$children[1].title; }, }; </script>
// 子組件1-ref方式 <template> <div> <div class="center">我的父組件是誰:{{content}}</div> </div> </template> <script> export default { name: 'demo1', data() { return { title: '我是子組件', content: null }; }, mounted() { this.content = this.$parent.content; }, methods: { sayHello() { window.alert('Hello'); } } }; </script>
// 子組件2-children方式 <template> <div> <div class="center"></div> </div> </template> <script> export default { name: 'demo1', data() { return { title: '我是子組件2' }; }, }; </script>
通過例子可以看到這兩種方式都可以父子間通訊,而缺點也很統一,就是都不能跨級以及兄弟間通訊。
小總結:父子組件間共享數據以及方法的便捷實踐之一。
總結
組件間不同的使用場景可以分為 3 類,對應的通訊方式如下:
- 父子通訊:Props / $emit,$emit / $on,Vuex,$attrs / $listeners,provide/inject,$parent / $children&$refs
- 兄弟通訊:$emit / $on,Vuex
- 隔代(跨級)通訊:$emit / $on,Vuex,provide / inject,$attrs / $listeners
大家可以根據自己的使用場景選擇不同的通訊方式,當然還是都自己寫寫程式碼,試驗一把來的印象深刻嘍。