Vue中組件間通信的方式


Vue中組件間通信的方式

Vue中組件間通信包括父子組件、兄弟組件、跨級組件、非嵌套組件之間通信。

props $emit

props $emit適用於父子組件的通信,這種組件通信的方式是我們運用的非常多的一種,props以單向數據流的形式可以很好的完成父子組件的通信,所謂單向數據流,就是數據只能通過props由父組件流向子組件,而子組件並不能通過修改props傳過來的數據修改父組件的相應狀態,所有的props都使得其父子props之間形成了一個單向下行綁定,父級props的更新會向下流動到子組件中,但是反過來則不行,這樣會防止從子組件意外改變父級組件的狀態,導致難以理解數據的流向而提高了項目維護難度。實際上如果傳入一個基本數據類型給子組件,在子組件中修改這個值的話Vue中會出現警告,如果對於子組件傳入一個引用類型的對象的話,在子組件中修改是不會出現任何提示的,但這兩種情況都屬於改變了父子組件的單向數據流,是不符合可維護的設計方式的。
正因為這個特性,而我們會有需要更改父組件值的需求,就有了對應的$emit,當我們在組件上定義了自定義事件,事件就可以由vm.$emit觸發,回調函數會接收所有傳入事件觸發函數的額外參數,$emit實際上就是是用來觸發當前實例上的事件,對此我們可以在父組件自定義一個處理接受變化狀態的邏輯,然后在子組件中如若相關的狀態改變時,就觸發父組件的邏輯處理事件。

父組件向子組件傳值

父組件向子組件傳值通過props傳遞值即可。

<!-- 子組件 -->
<template>
    <div>

        <div>我是子組件,接收:{{ msg }}</div>

    </div>
</template>

<script>
    export default {
        name: "child",
        components: {},
        props: ["msg"],
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>

<!-- 父組件 -->
<template>
    <div>

        <child :msg="msg"></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg: "父組件 Msg"
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
    
</style>

子組件向父組件傳值

子組件向父組件傳值需要通過事件的觸發,將更改值的行為傳遞到父組件去執行。

<!-- 子組件 -->
<template>
    <div>

        <div>我是子組件,接收:{{ msg }}</div>
        <button @click="$emit('changeMsg', '子組件傳值 Msg')">觸發事件並傳遞值到父組件</button>

    </div>
</template>

<script>
    export default {
        name: "child",
        components: {},
        props: ["msg"],
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<!-- 父組件 -->
<template>
    <div>

        <child 
            :msg="msg"
            @changeMsg="changeMsg"
        ></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg: "父組件 Msg"
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {
            changeMsg: function(msg){
                this.msg = msg;
            }
        }
    }
</script>

<style scoped>
    
</style>

v-model

v-model通常稱為數據雙向綁定,也可以稱得上是一種父子組件間傳值的方式,是當前組件與input等組件進行父子傳值,其本質上就是一種語法糖,通過props以及input(默認情況下)的事件的event中攜帶的值完成,我們可以自行實現一個v-model

<template>
    <div>

        <div>{{msg}}</div>
        <input :value="msg" @input="msg = $event.target.value">

    </div>
</template>

<script>
    export default {
        data: () => ({
            msg: "Msg"
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
    
</style>

sync修飾符

sync修飾符也可以稱為一個語法糖,在Vue 2.3之后新的.sync修飾符所實現的已經不再像Vue 1.0那樣是真正的雙向綁定,而是和v-model類似,是一種語法糖的形式,也可以稱為一種縮寫的形式,在下面父組件兩種寫法是完全等同的。

<!-- 子組件 -->
<template>
    <div>

        <div>我是子組件,接收:{{ msg }}</div>
        <button @click="$emit('update:msg', '子組件傳值 Msg')">觸發事件並傳遞值到父組件</button>

    </div>
</template>

<script>
    export default {
        name: "child",
        components: {},
        props: ["msg"],
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>

<!-- 父組件 -->
<template>
    <div>

        <child 
            :msg="msg1"
            @update:msg="value => msg1 = value"
        ></child>
        
        <child
            :msg.sync="msg2"
        ></child>
        

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg1: "父組件 Msg1",
            msg2: "父組件 Msg2",
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {
            changeMsg: function(msg){
                this.msg = msg;
            }
        }
    }
</script>

<style scoped>
    
</style>

provide inject

provide inject適用於父子組件以及跨級組件的通信,類似於ReactContext API,在父組件中通過provider來提供屬性,然后在子組件中通過inject來注入變量,不論子組件有多深,只要調用了inject那么就可以注入在provider中提供的數據,而不是局限於只能從當前父組件的props屬性來獲取數據,只要在父組件內定義的provide的數據,子組件都可以調用。當然Vue中注明了provideinject主要在開發高階插件/組件庫時使用,並不推薦用於普通應用程序代碼中。

<!-- 子組件 -->
<template>
    <div>
        <div>inject: {{msg}}</div>
    </div>
</template>

<script>
    export default {
        name: "child",
        inject: ["msg"],
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<template>
    <div>

        <child></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({

        }),
        provide: {
            msg: "provide msg"
        },
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
    
</style>

$attrs $listeners

$attrs $listeners適用於直接的父子組件通信並通過props傳遞可以實現跨級組件的通信,假設此時我們有三個組件分別為ABC,父組件A下面有子組件B,父組件B下面有子組件C,這時如果組件A直接想傳遞數據給組件C那就不能直接傳遞了,只能是組件A通過props將數據傳給組件B,然后組件B獲取到組件A傳遞過來的數據后再通過props將數據傳給組件C,當然這種方式是非常復雜的,無關組件中的邏輯業務增多了,代碼維護也沒變得困難,再加上如果嵌套的層級越多邏輯也復雜,無關代碼越多,針對這樣一個問題,Vue 2.4提供了$attrs$listeners來實現能夠直接讓組件A直接傳遞消息給組件C

<!-- 子子組件 -->
<template>
    <div>

        

    </div>
</template>

<script>
    export default {
        name: "child-child",
        components: {},
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {
            console.log(this.$attrs); // {param: 1, test: 2}
            console.log(this.$listeners); // {testEvent: ƒ}
        },
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<!-- 子組件 -->
<template>
    <div>
        <!-- 直接將剩余的參數傳遞給子組件 -->
        <child-child v-bind="$attrs" v-on="$listeners"></child-child>
    </div>
</template>

<script>
    import childChild from "./child-child";
    export default {
        name: "child",
        components: { childChild },
        props: ["msg"], // 聲明了接收名為msg的prop 此時在此組件的$attrs則不會再有msg參數
        data: () => ({
            
        }),
        inheritAttrs: false, // 默認設置為true也可 // 默認情況下true 父作用域的不被認作 props 的 attribute 綁定將會回退且作為普通的 HTML attribute 應用在子組件的根元素上。
        beforeCreate: function() {},
        created: function() {
            console.log(this.$attrs); // {param: 1, test: 2}
            console.log(this.$listeners); // {testEvent: ƒ}
        },
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<!-- 父組件 -->
<template>
    <div>

        <child 
            :msg="msg"
            :param="1"
            :test="2"
            @testEvent="tips"
        ></child>
        
    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg: "Msg",
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {
            tips: function(...args){
                console.log(args);
            }
        }
    }
</script>

<style scoped>
    
</style>

$children $parent

$children $parent適用於父子組件以及跨級組件的通信,這種方式就比較直觀了,直接操作父子組件的實例,$parent就是父組件的實例對象,而$children就是當前實例的直接子組件實例數組了,官方文檔的說明是子實例可以用this.$parent訪問父實例,子實例被推入父實例的$children數組中,節制地使用$parent$children它們的主要目的是作為訪問組件的應急方法,更推薦用propsevents實現父子組件通信。此外在Vue2之后移除的$dispatch$broadcast也可以通過$children$parent進行實現,當然不推薦這樣做,官方推薦的方式還是更多簡明清晰的組件間通信和更好的狀態管理方案如Vuex,實際上很多開源框架都還是自己實現了這種組件通信的方式,例如Mint UIElement UIiView等。

<!-- 子組件 -->
<template>
    <div>
        
    </div>
</template>

<script>
    export default {
        name: "child",
        data: () => ({
            
        }),
        beforeCreate: function() {},
        mounted: function() {
            console.log(this.$parent); // VueComponent {_uid: 2, ...}
            console.log(this.$children); // []
        },
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<!-- 父組件 -->
<template>
    <div>

        <child></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({

        }),
        beforeCreate: function() {},
        mounted: function() {
            console.log(this.$parent); // VueComponent {_uid: 1, ...}
            console.log(this.$children); // [VueComponent]
        },
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
    
</style>

$refs

$refs適用於父子組件通信,ref被用來給元素或子組件注冊引用信息,引用信息將會注冊在父組件的$refs對象上,如果在普通的DOM元素上使用,引用指向的就是DOM元素,如果用在子組件上,引用就指向組件實例。要注意的是因為ref本身是作為渲染結果被創建的,在初始渲染的時候是不能訪問它們的,此時它們還不存在,另外$refs也不是響應式的,因此也不應該試圖用它在模板中做數據綁定。

<!-- 子組件 -->
<template>
    <div>
        
    </div>
</template>

<script>
    export default {
        name: "child",
        data: () => ({
            
        }),
        beforeCreate: function() {},
        created: function() {},
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
 
</style>
<!-- 父組件 -->
<template>
    <div>

        <child ref="child"></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({

        }),
        beforeCreate: function() {},
        created: function() {},
        mounted: function(){
            console.log(this.$refs.child); // VueComponent {_uid: 3, ...}
        },
        filters: {},
        computed: {},
        methods: {}
    }
</script>

<style scoped>
    
</style>

EventBus

EventBus可以適用於任何情況的組件通信,在項目規模不大的情況下,完全可以使用中央事件總線EventBus 的方式,EventBus可以比較完美地解決包括父子組件、兄弟組件、隔代組件之間通信,實際上就是一個觀察者模式,觀察者模式建立了一種對象與對象之間的依賴關系,一個對象發生改變時將自動通知其他對象,其他對象將相應做出反應。所以發生改變的對象稱為觀察目標,而被通知的對象稱為觀察者,一個觀察目標可以對應多個觀察者,而且這些觀察者之間沒有相互聯系,可以根據需要增加和刪除觀察者,使得系統更易於擴展。首先我們需要實現一個訂閱發布類作為單例模塊導出,並掛載到Vue.prototype作為Vue實例中可調用的全局對象使用,當然作為Mixins全局靜態橫切也可以,或者每個需要的組件再進行import也可以,此外務必注意在組件銷毀的時候卸載訂閱的事件調用,否則會造成內存泄漏。

// 實現一個PubSub模塊
var PubSub = function() {
    this.handlers = {};
}

PubSub.prototype = {
    constructor: PubSub,
    on: function(key, handler) { // 訂閱
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(!this.handlers[key].includes(handler)) {
             this.handlers[key].push(handler);
             return true;
        }
        return false;
    },

    once: function(key, handler) { // 一次性訂閱
        if(!(key in this.handlers)) this.handlers[key] = [];
        if(this.handlers[key].includes(handler)) return false;
        const onceHandler = (...args) => {
            handler.apply(this, args);
            this.off(key, onceHandler);
        }
        this.handlers[key].push(onceHandler);
        return true;
    },

    off: function(key, handler) { // 卸載
        const index = this.handlers[key].findIndex(item => item === handler);
        if (index < 0) return false;
        if (this.handlers[key].length === 1) delete this.handlers[key];
        else this.handlers[key].splice(index, 1);
        return true;
    },

    commit: function(key, ...args) { // 觸發
        if (!this.handlers[key]) return false;
        console.log(key, "Execute");
        this.handlers[key].forEach(handler => handler.apply(this, args));
        return true;
    },

}

export default new PubSub();
<!-- 子組件 -->
<template>
    <div>

        <div>{{msg}}</div>
        <child></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg: "init"
        }),
        beforeCreate: function() {},
        created: function() {
            this.eventBus.on("ChangeMsg", this.changeMsg);
        },
        beforeDestroy: function(){
            this.eventBus.off("ChangeMsg", this.changeMsg);
        },
        filters: {},
        computed: {},
        methods: {
            changeMsg: function(msg){
                this.msg = msg;
            }
        }
    }
</script>

<style scoped>
    
</style>
<!-- 父組件 -->
<template>
    <div>

        <div>{{msg}}</div>
        <child></child>

    </div>
</template>

<script>
    import child from "./child";
    export default {
        components: { child },
        data: () => ({
            msg: "init"
        }),
        beforeCreate: function() {},
        created: function() {
            this.eventBus.on("ChangeMsg", this.changeMsg);
        },
        beforeDestroy: function(){
            this.eventBus.off("ChangeMsg", this.changeMsg);
        },
        filters: {},
        computed: {},
        methods: {
            changeMsg: function(msg){
                this.msg = msg;
            }
        }
    }
</script>

<style scoped>
    
</style>

Vuex

Vuex同樣可以適用於任何情況的組件通信,Vuex是一個專為Vue.js應用程序開發的狀態管理模式,其采用集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。
每一個Vuex應用的核心就是store倉庫,store基本上就是一個容器,它包含着你的應用中大部分的狀態stateVuex和單純的全局對象有以下兩點不同:

  • Vuex的狀態存儲是響應式的,當Vue組件從store中讀取狀態的時候,若store中的狀態發生變化,那么相應的組件也會相應地得到高效更新。
  • 不能直接改變store中的狀態,改變store中的狀態的唯一途徑就是顯式地提交mutation,這樣使得我們可以方便地跟蹤每一個狀態的變化。

實際上我們可以得到更多使用Vuex的優點:

  • 可以使用時間旅行功能。
  • Vuex專做態管理,由一個統一的方法去修改數據,全部的修改都是可以追溯的。
  • 在做日志搜集,埋點的時候,有Vuex更方便。
  • Vuex不會造成全局變量的污染,同時解決了父組件與孫組件,以及兄弟組件之間通信的問題。

當然如果項目足夠小,使用Vuex可能是繁瑣冗余的。如果應用夠簡單,最好不要使用Vuex,上文中的一個簡單的store模式就足夠了。
在下面例子中,我們通過提交mutation的方式,而非直接改變store.state.count,是因為我們想要更明確地追蹤到狀態的變化。這個簡單的約定能夠讓你的意圖更加明顯,這樣你在閱讀代碼的時候能更容易地解讀應用內部的狀態改變。此外這樣也讓我們有機會去實現一些能記錄每次狀態改變,保存狀態快照的調試工具。

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment: function(state) {
            state.count++;
        }
    }
})

store.commit("increment");
console.log(store.state.count); // 1

由於store中的狀態是響應式的,在組件中調用store中的狀態簡單到僅需要在計算屬性中返回即可。觸發變化也僅僅是在組件的methods中提交mutation即可。

import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);


const store = new Vuex.Store({
    state: {
        count: 0
    },
    mutations: {
        increment: state => state.count++,
        decrement: state => state.count--
    }
})

new Vue({
    el: "#app",
    store,  
    computed: {
        count: function() {
            return this.$store.state.count;
        }
    },
     methods: {
        increment: function() {
            this.$store.commit("increment");
        },
        decrement: function() {
            this.$store.commit("decrement");
        }
    }
})

每日一題

https://github.com/WindrunnerMax/EveryDay

參考

https://zhuanlan.zhihu.com/p/109700915
https://juejin.cn/post/6844903887162310669
https://juejin.cn/post/6844903784963899405
https://segmentfault.com/a/1190000022083517
https://github.com/yangtao2o/learn/issues/97
https://github.com/YangYmimi/read-vue/issues/12


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM