Vue 組件數據通訊方案總結

前言

組件是 vue.js 最強大的功能之一,而組件實例的做用域是相互獨立的,這就意味着不一樣組件之間的數據沒法相互引用。通常來講,組件能夠有如下幾種關係:html

組件關係圖

如上圖所示,grandfatherparentparentchildAparentchildB 都是父子關係,childAchildB 是兄弟關係,grandfatherchildAchildB 是隔代關係(可能隔多代)。
因此組件通信是 vue.js 的核心之一,接下來結合代碼,來了解各個組件的是怎麼通信的。vue

1、props$emit

子組件(Child.vue)的 props 屬性可以接收來自父組件(Parent.vue)數據。沒錯,僅僅只能接收,props是單向綁定的,即只能父組件向子組件傳遞,不能反向。node

// 父組件(Parent.vue)
<template>
    <div id="parent">
        <Child :msg="msg" />
    </div>
</template>

<script>
import Child from './Child'

export default {
    name: 'parent',
    data() {
        return {
            msg: '這是來自父組件來的數據~~'
        }
    },

    components: {
        Child
    }
}
</script>
// 子組件(Child.vue)
<template>
    <div id="child">
        <div>{{ msg }}</div>
    </div>
</template>

<script>
export default {
    name: 'child',
    data() {
        return {
        }
    },

    props: {
        msg: {
            type: String
        }
    },

    methods: {
    }
}
</script>

$emit 實現子組件向父組件傳值(經過事件形式),子組件經過 $emit 事件向父組件發送消息,將本身的數據傳遞給父組件。編程

// 父組件
<template>
    <div id="parent">
        <div>{{ msg }}</div>
        <Child2 @changeMsg="parentMsg" />
    </div>
</template>

<script>
import Child2 from './Child2'

export default {
    name: 'parent',
    data() {
        return {
            msg: ''
        }
    },

    methods: {
        parentMsg( msg ) {
            this.msg = msg;
        }
    },

    components: {
        Child2
    }
}
</script>
// 子組件
<template>
    <div id="child">
        <button @click="childMsg">傳遞數據給父組件</button>
    </div>
</template>

<script>
export default {
    name: 'child',
    data() {
        return {
        }
    },

    methods: {
        childMsg() {
            this.$emit( 'changeMsg', '傳遞數據給粑粑組件' );
        }
    }
}
</script>

總結:開發組件經常使用的數據傳輸方式,父子間傳遞。數組

2、$emit$on

實現方式是經過建立一個空的 vue 實例,當作 $emit 事件的處理中心(事件總線),經過它來觸發以及監聽事件,來實現任意組件間的通訊,包含父子,兄弟,隔代組件。異步

// 父組件
<template>
    <div id="parent">
        <Child1 :Event="Event" />
        <Child2 :Event="Event" />
        <Child3 :Event="Event" />
    </div>
</template>

<script>
import Vue from 'Vue';
import Child1 from './Child1';
import Child2 from './Child2';
import Child3 from './Child3';

// 公共的實例
const Event = new Vue();

export default {
    name: 'parent',
    data() {
        return {
            Event
        }
    },

    components: {
        Child1,
        Child2,
        Child3
    }
}
</script>
// 子組件1
<template>
    <div id="child1">
        一、她的名字叫:{{ name }}
        <button @click="send">傳遞數據給Child3</button>
    </div>
</template>

<script>
export default {
    name: 'child1',
    data() {
        return {
            name: '柯基慧'
        }
    },

    props: {
        Event: Object
    },

    methods: {
        send() {
            this.Event.$emit( 'msgA', this.name );
        }
    }
}
</script>
// 子組件2
<template>
    <div id="child2">
        一、她的身高:{{ height }}
        <button @click="send">傳遞數據給Child3</button>
    </div>
</template>

<script>
export default {
    name: 'child2',
    data() {
        return {
            height: '149.9cm'
        }
    },

    props: {
        Event: Object
    },

    methods: {
        send() {
            this.Event.$emit( 'msgB', this.height );
        }
    }
}
</script>
// 子組件3
<template>
    <div id="child3">
        <h3>她的名字叫:{{ name }},身高{{ height }}。</h3>
    </div>
</template>

<script>
export default {
    name: 'child3',
    data() {
        return {
            name: '',
            height: ''
        }
    },

    props: {
        Event: Object
    },

    mounted() {
        this.Event.$on( 'msgA', name => {
            this.name = name;
        } );

        this.Event.$on( 'msgB', height => {
            this.height = height;
        } );
    }
}
</script>

總結:在父子,兄弟,隔代組件中均可以互相數據通訊,重要的是 $emit 和 $on 事件必須是在一個公共的實例上才能觸發。ide

3、$attrs$listeners

Vue 組件間傳輸數據在 Vue2.4 版本後增長了新方法 $attrs 和 $listeners 。函數

$attrs

$attrs - 包含了父做用域中不做爲 props 被識別 (且獲取) 的特性綁定 ( class 和 style 除外)。當一個組件沒有聲明任何 props 時,這裏會包含全部父做用域的綁定 ( class 和style 除外),而且能夠經過 v-bind="$attrs" 傳入內部組件 - 在建立高級別的組件時很是有用。 簡單點講就是包含了因此父組件在子組件上設置的屬性(除了 props 傳遞的屬性、class 和 style )。性能

想象一下,你打算封裝一個自定義input組件 - MyInput,須要從父組件傳入 typeplaceholdertitle 等多個html元素的原生屬性。此時你的 MyInput 組件 props 以下:this

props:['type', 'placeholder', 'title', ...]

若是它的屬性越多,那子組件就要定義更多的屬性,會很影響閱讀,因此,$attrs 專門爲了解決這種問題而誕生,這個屬性容許你在使用自定義組件時更像是使用原生 html 元素。好比:

// 父組件
<template>
    <div id="parentAttrs">
        <MyInput placeholder="請輸入你的姓名" type="text" title="姓名" v-model="name" />
    </div>
</template>

<script>
import MyInput from './MyInput';

export default {
    name: 'parent',
    data() {
        return {
            name: ''
        }
    },

    components: {
        MyInput
    }
}
</script>
// 子組件
<template>
    <div>
        <label>姓名:</label>
        <input v-bind="$attrsAll" @input="$emit( 'input', $event.target.value )" />
    </div>
</template>
<script>
export default {
    name: 'myinput',
    data() {
        return {}
    },

    inheritAttrs: false,

    computed: {
        $attrsAll() {
            return {
                value: this.$vnode.data.model.value,
                ...this.$attrs
            }
        }
    }
}
</script>

$listener

$listeners - 包含了父做用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它能夠經過  v-on="$listeners" 傳入內部組件 - 在建立更高層次的組件時很是有用。 簡單點講它是一個對象,裏面包含了做用在這個組件上全部的監聽器(監聽事件),能夠經過 v-on="$listeners" 將事件監聽指向這個組件內的子元素(包括內部的子組件)。

同上面 $attrs 屬性同樣,這個屬性也是爲了在自定義組件中使用原生事件而產生的。好比要讓前面的 MyInput 組件實現 focus 事件,直接這麼寫是沒用的。

<template>
    <div id="parentListener">
        <MyInput @focus="focus" placeholder="請輸入你的姓名" type="text" title="姓名" v-model="name" />
    </div>
</template>

<script>
import MyInput from './MyInput';

export default {
    name: 'parent',
    data() {
        return {
            name: ''
        }
    },

    methods: {
        focus() {
            console.log( 'test' );
        }
    },

    components: {
        MyInput
    }
}
</script>

必需要讓 focus 事件做用於 MyInput 組件的 input 元素上。

<template>
    <div>
        <label>姓名:</label>
        <input v-bind="$attrsAll" v-on="$listenserAll" />
        <button @click="handlerF">操做test</button>
    </div>
</template>
<script>
export default {
    name: 'myinput',
    data() {
        return {}
    },

    inheritAttrs: false,

    props: ['value'],

    methods: {
        handlerF() {
            this.$emit( 'focus' );
        }
    },

    computed:{
         $attrsAll() {
            return {
                value: this.value,
                ...this.$attrs
            }
        },

        $listenserAll() {
            return Object.assign(
                {},
                this.$listeners,
                {input: event => this.$emit( 'input', event.target.value )})
        }
    }
}
</script>

$attrs 裏存放的是父組件中綁定的非 props 屬性,$listeners 裏面存放的是父組件中綁定的非原生事件。

組件能夠經過在本身的子組件上使用 v-on=」$listeners」,進一步把值傳給本身的子組件。若是子組件已經綁定 $listener 中同名的監聽器,則兩個監聽器函數會以冒泡的方式前後執行。

總結:用在父組件傳遞數據給子組件或者孫組件。

4、provideinject

Vue2.2 版本之後新增了這兩個 API, 這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在其上下游關係成立的時間裏始終生效。

使用方法:provide 在父組件中返回要傳給下級的數據;inject 在須要使用這個數據的子輩組件或者孫輩等下級組件中注入數據。

使用場景:因爲 vue 有 $parent 屬性可讓子組件訪問父組件。但孫組件想要訪問祖先組件就比較困難。經過 provide/inject 能夠輕鬆實現跨級訪問父組件的數據。

注意:provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。

// 父組件
<template>
    <div class="parentProvide">
        <button @click="changeSth">我要幹嗎好呢~</button>
        <p>要幹嗎:{{ sth }}</p>
        <ChildA />
    </div>
</template>

<script>
import ChildA from './ChildA';

export default {
    name: 'parent-pro',
    data() {
        return {
            sth: '吃飯~'
        }
    },

    // 在父組件傳入變量
    provide() {
        return {
            obj: this
        }
    },

    methods: {
        changeSth() {
            this.sth = '睡覺~';
        }
    },

    components: {
        ChildA
    }
}
</script>
// 子組件A
<template>
    <div>
        <div class="childA">
            <p>子組件A該幹嗎呢:{{ this.obj.sth }}</p>
        </div>
        <ChildB />
    </div>
</template>

<script>
import ChildB from "./ChildB";

export default {
    name: "child-a",
    data() {
        return {};
    },

    props: {},

    // 在子組件拿到變量
    inject: {
        obj: {
            default: () => {
                return {}
            }
        }
    },

    components: {
        ChildB
    }
}
</script>
// 子組件B
<template>
    <div>
        <div class="childB">
            <p>子組件B該幹嗎呢:{{ this.obj.sth }}</p>
        </div>
    </div>
</template>

<script>
export default {
    name: "child-b",
    data() {
        return {};
    },

    props: {},

    // 在子組件拿到變量
    inject: {
        obj: {
            default: () => {
                return {}
            }
        }
    }
}
</script>

總結:傳輸數據父級一次注入,子孫組件一塊兒共享的方式。

5、$parent$children & $refs

$parent 和 $children :指定已建立的實例之父實例,在二者之間創建父子關係。子實例能夠用 this.$parent 訪問父實例,子實例被推入父實例的 $children 數組中。

$refs :一個對象,持有註冊過 ref 特性的全部 DOM 元素和組件實例。ref 被用來給元素或子組件註冊引用信息。引用信息將會註冊在父組件的 $refs 對象上。若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子組件上,引用就指向組件。

// 父組件
<template>
    <div class="parentPC">
        <p>個人名字:{{ name }}</p>
        <p>個人標題:{{ title }}</p>
        <ChildA ref="comp1" />
        <ChildB ref="comp2" />
    </div>
</template>

<script>
import ChildA from "./ChildA.vue";
import ChildB from "./ChildB.vue";
export default {
    name: 'parent-pc',
    data() {
        return {
            name: '',
            title: '',
            contentToA: 'parent-pc-to-A',
            contentToB: 'parent-pc-to-B'
        }
    },

    mounted() {
        const comp1 = this.$refs.comp1;
        this.title = comp1.title;
        comp1.sayHi();
        this.name = this.$children[1].title;
    },

    components: {
        ChildA,
        ChildB
    }
}
</script>
// 子組件A - ref方式
<template>
    <div>
        <p>(ChildA)個人父組件是誰:{{ content }}</p>
    </div>
</template>

<script>
export default {
    name: 'child-a',
    data() {
        return {
            title: '我是子組件child-a',
            content: ''
        }
    },

    methods: {
        sayHi() {
            console.log( 'Hi, girl~' );
        }
    },

    mounted() {
        this.content = this.$parent.contentToA;
    }
}
</script>
// 子組件B - children方式
<template>
    <div>
        <p>(ChildB)個人父組件是誰:{{ content }}</p>
    </div>
</template>

<script>
export default {
    name: 'child-b',
    data() {
        return {
            title: '我是子組件child-b',
            content: ''
        }
    },

    mounted() {
        this.content = this.$parent.contentToB;
    }
}
</script>

從上面例子能夠看到這兩種方式均可以父子間通訊,而缺點就是都不能跨級以及兄弟間通訊。

總結:父子組件間共享數據以及方法的便捷實踐之一。

6、Vuex

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。

Vuex

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

一、State (index.js)

State 用來存狀態。在根實例中註冊了 store 後,用 this.$store.state 來訪問。

Vue.use(Vuex);

const state = {
    userInfo: {},           // 用戶信息
};

export default new Vuex.Store({
    state,
    getters,
    mutations,
    actions
});

二、Getters

Getters 從 State 上派生出來的狀態。能夠理解爲基於 State 的計算屬性。不少時候,不須要 Getters,直接用 State 便可。

export default {
    /**
    @description    獲取用戶信息
    */
    getUserInfo( states ) {
        return states.userInfo;
    }
}

三、Mutation

更改 Vuex 的 store 中的狀態的惟一方法是提交 Mutation

Mutation 用來改變狀態。須要注意的是,Mutation 裏的修改狀態的操做必須是同步的。在根實例中註冊了 store 後, 能夠用 this.$store.commit('xxx', data) 來通知 Mutation 來改狀態。

export const UPDATE_USERINFO = "UPDATE_USERINFO";

export default {
    [type.UPDATE_USERINFO]( states, obj ) {
        states.userInfo = obj;
    }
}

四、Action

  • Action 提交的是 Mutation,而不是直接變動狀態。
  • Action 能夠包含任意異步操做。

在根實例中註冊了 store 後, 能夠用 this.$store.dispatch('xxx', data) 來存觸發 Action

export default {
    update_userinfo({
        commit
    }, param) {
        commit( "UPDATE_USERINFO", param );
    }
}

乍一眼看上去感受畫蛇添足,咱們直接分發 Mutation 豈不更方便?實際上並不是如此,還記得 Mutation 必須同步執行這個限制麼?Action 就不受約束!咱們能夠在 Action 內部執行異步操做:

actions: {
  incrementAsync ({ commit }) {
    setTimeout(() => {
      commit('increment')
    }, 1000)
  }
}

總結:對 Vue 應用中多個組件的共享狀態進行集中式的管理(讀/寫),統一的維護了一份共同的 State 數據,方便組件間共同調用。

7、slot-scopev-slot

從 vue@2.6.x 開始,Vue 爲具名和範圍插槽引入了一個全新的語法,v-slot 指令。

一個假設的 <base-layout> 組件的模板以下:

<template>
    <div class="container">
        <header>
            <slot name="header"></slot>
        </header>

        <main>
            <slot></slot>
        </main>

        <footer>
            <slot name="footer"></slot>
        </footer>
    </div>
</template>

<script>
export default {
    name: "base-layout",
    data() {
        return {}
    }
}
</script>

在向具名插槽提供內容的時候,咱們能夠在一個父組件的 <template> 元素上使用 v-slot 特性:

// 父組件
<template>
    <base-layout>
        <template v-slot:header>
            <h1>Here might be a page title</h1>
        </template>

        <p>A paragraph for the main content.</p>
        <p>And another one.</p>

        <template v-slot:footer>
            <p>Here's some contact info</p>
        </template>
    </base-layout>
</template>

<script>
import BaseLayout from "./BaseLayout";

export default {
    name: "parent-slot",
    data() {
        return {
        }
    },

    components: {
        BaseLayout
    }
}
</script>

插槽的名字如今經過 v-slot:slotName 這種形式來使用,沒有名字的 <slot> 隱含有一個 "default" 名稱:

<template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
</template>

8、scopedSlots 屬性

scopedSlots 是編程式語法,在 render() 函數中使用 scopedSlots

// baseLayout.vue
<script>
export default {
    data() {
        return {
            headerText: "child header text",
            defaultText: "child default text",
            footerText: "child footer text"
        }
    },

    render( h ) {
        return h("div", { class: "child-node" }, [
            this.$scopedSlots.header({ text: this.headerText }),
            this.$scopedSlots.default(this.defaultText),
            this.$scopedSlots.footer({ text: this.footerText })
        ]);
    }
}
</script>
<script>
import BaseLayout from "./baseLayout";
export default {
    name: "ScopedSlots",
    components: {
        BaseLayout
    },

    render(h) {
        return h("div", { class: "parent-node" }, [
            this.$slots.default,
            h("base-layout", {
                scopedSlots: {
                    header: props => {
                        return h("p", { style: { color: "red" } }, [
                            props.text
                        ]);
                    },

                    default: props => {
                        return h("p", { style: { color: "deeppink" } }, [
                            props
                        ]);
                    },

                    footer: props => {
                        return h("p", { style: { color: "orange" } }, [
                            props.text
                        ]);
                    }
                }
            })
        ]);
    }
}
</script>

總結一下

組件間不一樣的使用場景能夠分爲 3 類,對應的通訊方式以下:

父子通訊props 和 $emit$emit 和 $onVuex$attrs 和 $listenersprovide 和 inject$parent 和 $children$refs

兄弟通訊$emit 和 $onVuex

隔代(跨級)通訊$emit 和 $onVuexprovide 和 inject$attrs 和 $listeners

相關文章
相關標籤/搜索