一、什么是組件通信
1、通信是組件或模塊之間的數據交互。
2、組件多重通信就形成了數據流,數據流管理的優劣決定了產品能否上線,返工是否頻繁的問題。
3、Vue中有哪些常見的通信方案?
組件樹的概念:
在Vue中,組件樹(Component Tree)指的是一個由多個嵌套的Vue組件組成的層級結構。每個組件可以是另一個組件的子組件,這樣層層嵌套形成一棵樹。這種結構允許開發者構建復雜和可重用的用戶界面。
(1)父子組件通信
父傳子使用自定義屬性(使用props接收),子傳父使用自定義事件(使用$emit)。
(2)狀態提升
狀態就是數據的意思。
當兄弟組件之間需要共享數據時,我們通常的做法是把這個數據定義在它們的共同的父組件中,再通過自定義屬性實現數據共享。
(3)provide/inject通信
這是一種在組件樹間自上而下的一種數據通信方案。
也就是說,只能從父級組件中向后代組件傳遞。
是Vue提供的兩個內置選項,可以對象寫法,也可以function寫法。
需要注意的是,當provide提供動態數據(聲明式變量)時,動態數據發生變化,后代組件們不會自動更新,這是為什么呢?
因為生命周期流程中,是在beforeCreate()和created()之間,inject注入provide數據。
所以下面在例子代碼里,更新父組件中的provide數據,子組件不會更新!!!
(4)ref通信
ref是Vue內置的一個屬性,就像id、class一樣,每一個HTML元素或組件都有這個屬性。
ref作用在HTML元素上,得到DOM實例,ref作用在組件上得到組件實例。
使用ref訪問組件實例,可以進一步訪問組件中的數據和方法。
ref是一種快捷的DOM訪問方式,當然ref也可以作用在組件上得到組件實例對象。這些ref得到的DOM實例或組件實例,使用this.$refs來訪問它們。
ref盡量少用,除非某些難搞的需求。
(5)slot插槽通信
從子組件向父組件傳遞數據,在父組件的插槽中,使用#default='scoped'訪問子組件<slot>回傳的數據。
子組件訪問插槽,借助this.$slots訪問父組件中的插槽實例。
這種通信在組件庫中、工作中,非常常見。
注意:在自定義插槽<comp-child-e>包起來的內容是父組件的。
(6)$parent/$children通信
借助$parent/$children這兩個API,可以實現在任一組件中訪問組件樹中的其它任意組件實例。可以做到在組件中隨意穿梭。
$parent表示的是當前組件的父組件實例對象。
$children表示的是當前組件的子組件們。
(7)$attrs/$listeners通信
借助$attrs可以訪問父組件傳遞過來的自定義屬性(除了class和style外)。
借助$listeners可以訪問父組件給的自定義事件。
在某些場景下,$attrs/$listeners可以替代props/$emit()這種通用的通信方式。
(8)事件總線
借助Vue內置的事件系統。($on/$emit/$off/$once)實現“訂閱-發布”模式的通信。這種通信方式是一對多的,且與組件的層級無關。
(9)Vuex通信
這是Vue架構中終極的通信方案,也是Vue架構中用的最多的一種通信方式。
二、狀態提升例子
有一個變量大哥點了加1,二哥點了減1。
<html>
<head><title>組件通信-狀態提升</title><style></style>
</head>
<body><div id="app"><comp-child-a :numa="num" @add="num+=$event"></comp-child-a><hr><comp-child-b :numb="num" @sub="num-=$event"></comp-child-b><hr></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-a', {template: `<div><div>大哥 {{numa}}</div><button @click='$emit("add", 1)'>加1</button></div>`,props: {numa: {type: Number, default: 0}}})Vue.component('comp-child-b', {template: `<div><div>二哥 {{numb}}</div><button @click='$emit("sub", 1)'>減1</button></div>`,props: {numb: {type: Number, default: 0}}})const app = new Vue({// 變量定義在父組件中data() {return {num: 1}}})app.$mount('#app')</script></body>
</html>
三、provide/inject例子
注意:provide提供聲明式變量numf,如果在父組件中修改了app.num的值,子組件中不會改變!!!
<html>
<head><title>組件通信-provide/inject</title><style></style>
</head>
<body><div id="app"><comp-child-c></comp-child-c></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-c', {template: `<div><div>三哥 {{msg}} - {{numf}}</div><span v-for='i in list' v-text='i'></span></div>`,// 從組件樹的上下文中注入provide的數據inject: ['msg','list','numf']})const app = new Vue({// 變量定義在父組件中data() {return {num: 1}},// 從當前組件節點開始,向后代組件們傳遞數據// 工作中更推薦function寫法,因為可以用this// provide: {// msg: "你好",// list: [1,2,3,4]// }provide() {return {msg: '你好',list: [1,2,3,4],numf: this.num // 動態數據}}})app.$mount('#app')</script></body>
</html>
四、ref例子
<html>
<head><title>組件通信-ref</title><style></style>
</head>
<body><div id="app"><div ref='box'>普通HTML標簽</div><comp-child-d ref='dd'></comp-child-d><button @click='handle'>測試ref操作</button></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-d', {template: `<div><div>四哥 {{age}}</div></div>`,data() {return {age: 10}},methods: {changeAge(arg) {this.age = arg || 10}}})const app = new Vue({// 變量定義在父組件中data() {return {num: 1}},methods: {handle() {console.log('---refs', this.$refs)this.$refs.box.style.color = 'red'this.$refs.dd.changeAge(20)}}})app.$mount('#app')</script></body>
</html>
點擊按鈕:
獲取到DOM實例和組件實例:
五、插槽例子
在Vue中,凡是被包裹在自定義組件內部的東西,都是插槽內容。
問題:插槽加上作用域后,子組件內插槽內的this.$slots拿不到父組件內的插槽內容?
解答:加了作用域后,要取$scopedSlots屬性。
<html>
<head><title>組件通信-插槽</title><style></style>
</head>
<body><div id="app"><comp-child-e><template #default='scoped'><div>別墅一棟 {{scoped.foo}} {{scoped.bar}}</div></template><template #car='scoped'><div>{{scoped.car2}}專用車位</div></template></comp-child-e></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-e', {template: `<div><div>五哥</div><slot name='default' foo='你好' bar='世界'><div>我的房子</div> </slot><slot name='car' :car2='car1'><div>我的車位</div></slot></div>`,data() {const cars = ['寶馬','奔馳']const val = cars[Math.floor(Math.random()*2)]return {car1: val}},mounted() {// 在子組件中使用this.$slots來訪問父組件給的插槽內容//console.log('---this', this)console.log('---slot', this.$slots)console.log('---scoped slot', this.$scopedSlots)}})const app = new Vue({})app.$mount('#app')</script></body>
</html>
頁面顯示:
控制臺:
六、$parent/$children例子
<html>
<head><title>組件通信-$parent/$children</title><style></style>
</head>
<body><div id="app"><comp-child-f></comp-child-f></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-f', {template: `<div><div>六哥 {{num}}</div></div>`,data() {return {num: 1}},methods: {add() {this.num++}}})const app = new Vue({mounted() {console.log('app---$parent', this.$parent)console.log('app---$children', this.$children)}})app.$mount('#app')</script></body>
</html>
頁面顯示:
控制臺修改num:
頁面變化:
七、$attrs/$listeners例子
<html>
<head><title>組件通信-$attrs/$listeners</title><style>.page span {display: inline-block;padding: 0 10px;cursor: pointer;}.page span.on {color: red;}</style>
</head>
<body><div id="app"><comp-child-g :page='page' @change='changePage'></comp-child-g></div><script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script><script>Vue.component('comp-child-g', {template: `<div class="page"><div>七哥</div><span v-for='i in pageArr' v-text='i' :class='{on:$attrs.page===i}' @click='$listeners.change(i)'></span></div>`,// 這里不用props接收,用attrsmounted() {console.log('g---attrs', this.$attrs)console.log('g---listeners', this.$listeners)},computed: {pageArr() {const p = this.$attrs.pageif (p<=3) return [1,2,3,4,5]else return [p-2,p-1,p,p+1,p+2]}}})const app = new Vue({data() {return {page: 1}},methods: {changePage(reg) {this.page = reg || 1}}})app.$mount('#app')</script></body>
</html>
頁面顯示:
控制臺修改page:
頁面變化: