Vue中元件間通訊的方式

WindrunnerMax發表於2021-01-28

Vue中元件間通訊的方式

Vue中元件間通訊包括父子元件、兄弟元件、隔代元件之間通訊。

props $emit

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

父元件向子元件傳值

父元件向子元件傳值通過prop傳遞值即可。

<!-- 子元件 -->
<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="msg1 = $event"
        ></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

類似於ReactContext API,在父元件中通過provider來提供屬性,然後在子元件中通過inject來注入變數,不論子元件有多深,只要呼叫了inject那麼就可以注入在provider中提供的資料,而不是侷限於只能從當前父元件的prop屬性來獲取資料,只要在父元件內的資料,子元件都可以呼叫。當然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

這種元件通訊的方式適合直接的父子元件,假設此時我們有三個元件分別為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

這種方式就比較直觀了,直接操作父子元件的例項,$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>

EventBus

在專案規模不大的情況下,完全可以使用中央事件匯流排EventBus 的方式,EventBus可以比較完美地解決包括父子元件、兄弟元件、隔代元件之間通訊,實際上就是一個觀察者模式,觀察者模式建立了一種物件與物件之間的依賴關係,一個物件發生改變時將自動通知其他物件,其他物件將相應做出反應。所以發生改變的物件稱為觀察目標,而被通知的物件稱為觀察者,一個觀察目標可以對應多個觀察者,而且這些觀察者之間沒有相互聯絡,可以根據需要增加和刪除觀察者,使得系統更易於擴充套件。首先我們需要實現一個訂閱釋出類,並作為全域性物件掛載到Vue.prototype,作為Vue例項中可呼叫的全域性物件使用,此外務必注意在元件銷燬的時候解除安裝訂閱的事件呼叫,否則會造成記憶體洩漏。

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

PubSub.prototype = {

    on: function(key, handler) { // 訂閱
        if (!(key in this.handlers)) this.handlers[key] = [];
        this.handlers[key].push(handler);
    },

    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;
        this.handlers[key].forEach(handler => handler.apply(this, args));
        return true;
    },

}

export { PubSub }
export default { 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是一個專為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

相關文章