天天看點

vue元件間的九種通信方式

作者:前端技術唠嗑

前言

Vue元件執行個體間的作用域是互相獨立的,而通常一個頁面是由很多個元件構成,這些元件可能又嵌套了元件,形成了一個關系網圖,它們的關系可能是像下圖中一樣,大緻分為兩種使用場景,父子元件間通信和非父子元件間通信,父子元件間通信又分為直接父子關系和間接父子關系。vue提供了多種通信方法,針對不同的通信需求,選擇最合适的通信方式能幫助我們提高開發效率。本文簡要介紹了九種通信方式以及适用場景,記錄下在學習中的收貨。

vue元件間的九種通信方式

父子元件間的通信

props和$emit

這是父子元件在傳參時中常用的一種方式,父元件通過v-bind傳入參數,子元件通過props來接收,子元件通過内建的$emit方法傳入事件名來觸發一個事件,父元件通過v-on像監聽原生DOM事件一樣來監聽這個事件。

// 父元件CompFather
<template>
    <div>
        <comp-son
            :title-name="title_name"
            @changeTitle="changeTitle"
        />
        <div>父元件的title_name:{{ title_name }}</div>
    </div>
</template>

<script>
    import CompSon from "./CompSon";
    export default {
        name: "CompFather",
        components: {CompSon},
        data() {
            return {
                title_name: "我是初始值"
            }
        },
        methods: {
            changeTitle(val) {
                this.title_name = val;
            }
        }
    }
</script>
複制代碼           
// 子元件CompSon
<template>
   <div>
      <div>子元件的titleName:{{titleName}}</div>
      <button @click="changeTitle">改變</button>
   </div>
</template>
​
<script>
   export default {
      name: "CompSon",
      props: {
         titleName: {
            type: String,
            required: true
         }
      },
      methods: {
         changeTitle() {
            this.$emit("changeTitle", "我變了");
         }
      }
   }
</script>
複制代碼           

适用場景:适用于直接父子關系(中間無嵌套元件)的元件間進行通信。

$attrs和$listeners

$attrs包含了父作用域中不作為 prop 被識别 (且擷取) 的 attribute 綁定 (class 和 style 除外)。當一個元件沒有聲明任何 prop 時,這裡會包含所有父作用域的綁定 (class 和 style 除外),并且可以通過 v-bind="$attrs" 傳入内部元件。

// Comp1.vue
<template>
   <comp2
      :flag="flag"
      :id="id"
      :msg="msg"
   />
</template>
​
<script>
   import Comp2 from "./comp2";
   export default {
      name: "comp1",
      components: {Comp2},
      data() {
         return {
            id: 1,
            msg: "comp1's msg",
            flag: true
         }
      }
   }
</script>
複制代碼           
// Comp2.vue
<template>
   <div>
      <div>comp2的$attrs:{{$attrs}}</div>
      <comp3 v-bind="$attrs" />
   </div>
</template>
​
<script>
   import Comp3 from "./comp3";
   export default {
      name: "comp2",
      components: {Comp3},
      props: {
         flag: Boolean
      },
      mounted() {
         console.log(this.$attrs); // { "id": 1, "msg": "comp1's msg" }
      }
   }
</script>
複制代碼           
// Comp3.vue
<template>
   <div>comp3的$attrs:{{ $attrs }}</div>
</template>
​
<script>
   export default {
      name: "comp3",
      props: {
         msg: String
      },
      mounted() {
         console.log(this.$attrs);  // { "id": 1 }
      }
   }
</script>
複制代碼           

這裡Comp2的props接收了Comp1傳入的flag,是以Comp2中的$attrs沒有flag參數,Comp3接收了msg,是以$attrs隻有id。

适用場景:元件之間跨級傳參,可以使用$attrs屬性,這樣使得代碼更加簡潔,更利于維護。

$listeners包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入内部元件。

當我們要封裝一個input類型的元件時,我們可以通過.native的方式監聽它的原生事件。

// App.vue
<my-input @input.native="inputData" :value="value" />
複制代碼           
// MyInput.vue
<template>
   <input
         type="text"
         :value="value"
         @input="$emit('input',e.target.value)"
      >
</template>
​
<script>
   export default {
      name: "MyInput",
      props: {
         value: String
      },
   }
</script>
複制代碼           

但如果此時的input被label标簽包裹了一層,input不再是根元件時,父元件的.native将監聽失效,于是vue給我們提供了$listeners屬性來解決這個問題。

// MyInput.vue
<template>
   <label>
      value:{{ value }}
      <input
         type="text"
         :value="value"
         @input="$emit('input',e.target.value)"
      >
   </label>
</template>
複制代碼           

以下是使用$listeners屬性進行監聽:

// App.vue
<template>
   <my-input v-model="value" @focus="onFocus" @input="inputData" />
</template>
​
<script>
   export default {
      name: "App",
      data() {
          value: ""
      },
       onFocus() {
            console.log("獲得焦點");
        },
​
        inputData(val) {
            console.log(val);
        }
   }
</script>
複制代碼           
<template>
   <label>
      value:{{ value }}
      <input
         type="text"
         :value="value"
         v-on="inputListeners"
      >
   </label>
</template>
​
<script>
   export default {
      name: "MyInput",
      props: {
         value: String
      },
      computed: {
         inputListeners: function () {
            const vm = this
            // `Object.assign` 将所有的對象合并為一個新對象
            return Object.assign({},
               // 我們從父級添加所有的監聽器
               this.$listeners,
               // 然後我們添加自定義監聽器,
               // 或覆寫一些監聽器的行為
               {
                  // 這裡確定元件配合 `v-model` 的工作
                  input: function (event) {
                     vm.$emit('input', event.target.value)
                  }
               }
            )
         }
      },
   }
</script>
複制代碼           

适用場景:當需要監聽子元件的原生事件,且子元件可能是經過重構的元件時,可以使用$listeners來代替.native監聽,當然也不隻是包含原生事件監聽器,隻是在這裡監聽原生事件比較合适,我們還有其它方式來做事件監聽。

$children/$parent/$root

$children是目前執行個體的直接子元件,不保證順序,數組順序不一定是子元件在該父元件中的渲染順序,也不是響應式的。

如果目前執行個體有父元件的話,$parent則是目前執行個體的父執行個體。

$root是目前元件樹的根 Vue 執行個體。如果目前執行個體沒有父執行個體,此執行個體将會是其自己。通過this.$root來通路。

<script>
   export default {
      name: "MyComp",
      data() {
          value: ""
      },
      mounted: {
        console.log(this.$parent);
        console.log(this.$children);
        console.log(this.$root);
      }
   }
</script>
複制代碼           

缺點:$children擷取到的執行個體不保證順序,是以當有多個子元件時可能擷取到的不是自己想要的那一個。當元件嵌套多級時,可能會出現$parent.$parent.$parent...的情況,對于後續維護不友好。

适用場景:盡量使用其它方式來代替$parent/$chilren

ref

ref 被用來給元素或子元件注冊引用資訊。引用資訊将會注冊在父元件的 $refs 對象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子元件上,引用就指向元件執行個體。

// App.vue
<template>
  <comp1 ref="comp1"/>
</template>
​
<script>
   export default {
      name: "App",
      data() {
          value: ""
      },
      mounted: {
          console.log("comp1的msg:",this.$refs.comp1.msg);
      }
   }
</script>
複制代碼           

适用場景:當使用element-ui元件時,可用于調用元件方法,例如el-table元件的選擇表格項,排序等等。

v-model

一個元件上的 v-model 預設會利用名為 value 的 prop 和名為 input 的事件,但是像單選框、複選框等類型的輸入控件可能會将 value attribute 用于不同的目的。model 選項可以用來避免這樣的沖突。

// App.vue
<add-item v-model="input_val" />
複制代碼           
<template>
   <div class="add">
      <input
         :value="value"
         type="text"
         placeholder="What needs to be done?"
         @input="inputVal"
      >
   </div>
</template>
​
<script>
   export default {
      name: "AddItem",
      // 如果對應的props字段名不叫value,則需要定義model屬性來指定父元件的v-modal綁定的是哪個值
      /*model: {
         prop: "value1",
         event: "input"
      },*/
      props: {
         value: {
            type: String,
            required: true
         }
      },
      methods: {
         inputVal(e) {
            this.$emit("input", e.target.value);
         }
      }
   }
</script>
複制代碼           

适用場景:v-model适用于在封裝input類型的元件時,用于給資料進行雙向綁定,如果不使用v-model,則需要在父元件增加一個方法來監聽事件然後改變父元件中的值,顯得非常麻煩且不簡潔。

Sync修飾符

sync與v-model非常類似,也是适用于需要子元件改變父元件的值時。 :isShow.sync="isShow"其實是 @update:isShow="bol=>isShow=bol"文法糖

<template>
    <div>
        <input type="button"
               value="我是父元件中的按鈕"
               @click="show">
        <child :isShow.sync="isShow" v-show="isShow"/>
    </div>
</template>
<script>
    import child from "@/components/child"
    export default {
        data() {
            return {
                isShow:false
            }
        },
        components:{
            child
        },
        methods:{
            show(){
                this.isShow=true;
            },
            changeIsShow(bol){
                this.isShow=bol;
            }
        }
    }
</script>
複制代碼           
<template>
    <div>
         我是一個子元件,我在紅色的海洋裡!
        <input type="button" value="點我隐身" @click="upIsShow">
    </div>
</template>
<script>
    export default {
        methods:{
            upIsShow(){
                this.$emit("update:isShow",false);
            }
        }
    }
</script>
複制代碼           

provide/inject

這對選項需要一起使用,以允許一個祖先元件向其所有子孫後代注入一個依賴,不論元件層次有多深,并在其上下遊關系成立的時間裡始終生效。

provide在父元件中将子元件要使用的資料抛出,然後在子元件中inject注入要使用的資料,它不像使用props需要層層傳遞,中間不需要這個資料的地方也需要聲明,provide和inject隻注重“源頭”和“終點”,主要用于在高階元件庫中使用,在平常的開發中一般不适用的原因是不友善追溯“源頭”,不知道是哪一層聲明的和使用了。

子孫層中的provide會覆寫祖父層中相同key的屬性值。

provide 選項應該是一個對象或傳回一個對象的函數。該對象包含可注入其子孫的 property。

inject選項應該是一個字元串數組或一個對象,from是選自祖父元件provide的哪個值的key,default指定一個預設值。

// 父元件
provide: ["message"],
    
// 子元件
inject: {
    msg: {
        from: "message",
        default: "default value"
    }
}
複制代碼           
// 父元件CompFather
<template>
   <div>
      <comp-son />
   </div>
</template>
​
<script>
   import CompSon from "./CompSon";
   export default {
      name: "CompFather",
      components: {CompSon},
      provide: {
         msg: "父元件msg"
      }
   }
</script>
複制代碼           
// 子元件 CompSon
<template>
   <div>
      子元件
   </div>
</template>
​
<script>
   export default {
      name: "CompSon",
      inject: ["msg"],
      mounted() {
         console.log(this.msg);  // 父元件msg
      }
   }
</script>
複制代碼           

provide 和 inject 綁定并不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監聽的對象,那麼其對象的 property 還是可響應的。例如provide和inject的是一個對象時,該資料是可響應的。

// 當provide/inject的是一個對象時,是可響應的,子元件改變值,父元件也會改變,父元件改變子元件也會改變
provide() {
   return {
      msg: this.msg
   }
},
    
data() {
    return {
        msg: {
            title: "父元件msg"
        }
    }
}
複制代碼           

适用場景:适用于封裝高階元件,祖先執行個體不關心哪個後代執行個體會用到,後代執行個體不關心資料來源于哪個祖先執行個體。

非父子元件間的通信

中央事件總線 bus

使用中央事件總線實際就是建立一個vue執行個體,利用這個vue執行個體來傳遞消息。

使用方式一:

// 定義一個bus檔案
import Vue from "vue";
const bus = new Vue();
export default bus;
複制代碼           
// 引入bus檔案
import bus from "@/bus";
bus.$emit("myEvent", "bus msg")
複制代碼           
// 引入bus檔案
import bus from "@/bus";
bus.$on("myEvent", data => {
    console.log(data);
});
複制代碼           

使用方式二:

// main.js
import Vue from 'vue'
import App from './App.vue'
​
Vue.prototype.$bus = new Vue();
​
new Vue({
  render: h => h(App),
}).$mount('#app')
​
複制代碼           
// 發送事件
this.$bus.$emit("myEvent", "bus msg");
// 接收事件
this.$bus.$on("myEvent", data => {
    console.log(data);
})
複制代碼           

适用場景:适用于不是特别大的單頁面應用跨級跨兄弟元件間通信。

VueX

當我們在開發大型但單頁應用時,元件間需要頻繁使用修改某些值,這時候使用元件間傳參就顯得非常繁雜混亂,且不利于管理維護,vue為解決這一問題提供了VueX這個狀态管理工具,我們隻需要将多個元件需要共享的狀态放VueX中進行統一管理,形成一個全局單例管理模式,即可實作元件資料同步。

vue元件間的九種通信方式

VueX的将要統一管理的狀态放在state中,這樣在引用了VueX的頁面中就能擷取到狀态,actions主要是處理例如背景請求的一些異步操作,保證資料的同步,通過dispatch方法調用actions中的方法,當得到異步的資料之後再通過commit調用mutations中的方法改變state。不要在元件中通過$store.state直接修改state資料,這樣Devtools是監控不到的,要經過mutations才可以。

這裡隻是簡要介紹通信方式,詳細的使用方法參見

[VueX官網] vuex.vuejs.org/zh/

總結

通信方式 适用場景
props/$emit 直接父子元件傳值
$attrs/$listeners $attrs非直接父子元件間傳值,$listeners監聽原生事件
$children/$parent 一般用其它方式代替,不友善維護
可用于調用高階元件方法,如注冊element-ui元件引用
封裝需要雙向綁定的元件時用v-modal進行傳參
sync修飾符 用于父子元件間,子元件需要改變父元件值時
多用于高階元件庫,平常開發一般不使用,不利于代碼維護
中央事件總線 适用于跨級或兄弟元件間通信。
用于大型單頁面中多個元件需要共享同一狀态時。

繼續閱讀