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 被識別 (且獲取) 的特性綁定 (ClassStyle 除外)。當一個組件沒有聲明任何 Prop 時,這裡會包含所有父作用域的綁定 (ClassStyle 除外),並且可以通過 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

大家可以根據自己的使用場景選擇不同的通訊方式,當然還是都自己寫寫程式碼,試驗一把來的印象深刻嘍。