【乾貨】Vue2.x 元件通訊方式詳解,這篇講全了

2023-04-27 12:00:18

前言

vue是資料驅動檢視更新的框架, 我們平時開發,都會把頁面不同模組拆分成一個一個vue元件, 所以對於vue來說元件間的資料通訊非常重要,那麼元件之間如何進行資料通訊的呢?

首先我們需要知道在vue中元件之間存在什麼樣的關係, 才更容易理解他們的通訊方式。

一般我們分為如下關係:

父子元件之間通訊
非父子元件之間通訊(兄弟元件、隔代關係元件、跨層級元件等)

Vue2.x 元件通訊共有12種

  1. props
  2. $emit / v-on
  3. .sync
  4. v-model
  5. ref
  6. $children / $parent
  7. $attrs / $listeners
  8. provide / inject
  9. EventBus
  10. Vuex
  11. $root
  12. slot
  13. 路由傳參
  14. observable

父子元件通訊可以用:

  • props
  • $emit / v-on
  • $attrs / $listeners
  • ref
  • .sync
  • v-model
  • $children / $parent

兄弟元件通訊可以用:

  • EventBus
  • Vuex
  • $parent

跨層級元件通訊可以用:

  • provide/inject
  • EventBus
  • Vuex
  • $attrs / $listeners
  • $root

Vue2.x 元件通訊使用寫法

下面把每一種元件通訊方式的寫法一一列出

1. props

父元件向子元件傳送資料,這應該是最常用的方式了

子元件接收到資料之後,不能直接修改父元件的資料。會報錯,所以當父元件重新渲染時,資料會被覆蓋。如果子元件內要修改的話推薦使用 computed

格式:

// 陣列:不建議使用
props:[]

// 物件
props:{
 inpVal:{
  type:Number, //傳入值限定型別
  // type 值可為String,Number,Boolean,Array,Object,Date,Function,Symbol
  // type 還可以是一個自定義的建構函式,並且通過 instanceof 來進行檢查確認
  required: true, //是否必傳
  default:200,  //預設值,物件或陣列預設值必須從一個工廠函數獲取如 default:()=>[]
  validator:(value) {
    // 這個值必須匹配下列字串中的一個
    return ['success', 'warning', 'danger'].indexOf(value) !== -1
  }
 }
}

事例:

// Parent.vue 傳送
<template>
    <child :msg="msg"></child>
</template>

// Child.vue 接收
export default {
  // 寫法一 用陣列接收
  props:['msg'],
  // 寫法二 用物件接收,可以限定接收的資料型別、設定預設值、驗證等
  props:{
      msg:{
          type:String,
          default:'這是預設資料'
      }
  },
  mounted(){
      console.log(this.msg)
  },
}

注意:

prop 只可以從上一級元件傳遞到下一級元件(父子元件),即所謂的單向資料流。而且 prop 唯讀,不可被修改,所有修改都會失效並警告。

  • 第一,不應該在一個子元件內部改變 prop,這樣會破壞單向的資料繫結,導致資料流難以理解。如果有這樣的需要,可以通過 data 屬性接收或使用 computed 屬性進行轉換。
  • 第二,如果 props 傳遞的是參照型別(物件或者陣列),在子元件中改變這個物件或陣列,父元件的狀態會也會做相應的更新,利用這一點就能夠實現父子元件資料的「雙向繫結」,雖然這樣實現能夠節省程式碼,但會犧牲資料流向的簡潔性,令人難以理解,最好不要這樣去做。
  • 想要實現父子元件的資料「雙向繫結」,可以使用 v-model 或 .sync。

2. $emit / v-on

子傳父的方法,子元件通過派發事件的方式給父元件資料,或者觸發父元件更新等操作

// Child.vue 派發
export default {
  data(){
      return { msg: "這是發給父元件的資訊" }
  },
  methods: {
      handleClick(){
          this.$emit("sendMsg",this.msg)
      }
  },
}
// Parent.vue 響應
<template>
    <child v-on:sendMsg="getChildMsg"></child>
    // 或 簡寫
    <child @sendMsg="getChildMsg"></child>
</template>

export default {
    methods:{
        getChildMsg(msg){
            console.log(msg) // 這是父元件接收到的訊息
        }
    }
}

3. v-model

和 .sync 類似,可以實現將父元件傳給子元件的資料為雙向繫結,子元件通過 $emit 修改父元件的資料

// Parent.vue
<template>
    <child v-model="value"></child>
</template>
<script>
export default {
    data(){
        return {
            value:1
        }
    }
}

// Child.vue
<template>
    <input :value="value" @input="handlerChange">
</template>
export default {
    props:["value"],
    // 可以修改事件名,預設為 input
    model:{
        // prop:'value', // 上面傳的是value這裡可以不寫,如果屬性名不是value就要寫
        event:"updateValue"
    },
    methods:{
        handlerChange(e){
            this.$emit("input", e.target.value)
            // 如果有上面的重新命名就是這樣
            this.$emit("updateValue", e.target.value)
        }
    }
}
</script>

4. ref

ref 如果在普通的DOM元素上,參照指向的就是該DOM元素;

如果在子元件上,參照的指向就是子元件範例,然後父元件就可以通過 ref 主動獲取子元件的屬性或者呼叫子元件的方法

// Child.vue
export default {
    data(){
        return {
            name:"RDIF"
        }
    },
    methods:{
        someMethod(msg){
            console.log(msg)
        }
    }
}

// Parent.vue
<template>
    <child ref="child"></child>
</template>
<script>
export default {
    mounted(){
        const child = this.$refs.child
        console.log(child.name) // RDIF
        child.someMethod("呼叫了子元件的方法")
    }
}
</script>

5. .sync

可以幫我們實現父元件向子元件傳遞的資料 的雙向繫結,所以子元件接收到資料後可以直接修改,並且會同時修改父元件的資料

// Parent.vue
<template>
    <child :page.sync="page"></child>
</template>
<script>
export default {
    data(){
        return {
            page:1
        }
    }
}

// Child.vue
export default {
    props:["page"],
    computed(){
        // 當我們在子元件裡修改 currentPage 時,父元件的 page 也會隨之改變
        currentPage {
            get(){
                return this.page
            },
            set(newVal){
                this.$emit("update:page", newVal)
            }
        }
    }
}
</script>

6. $attrs / $listeners

多層巢狀元件傳遞資料時,如果只是傳遞資料,而不做中間處理的話就可以用這個,比如父元件向孫子元件傳遞資料時。

$attrs:包含父作用域裡除 class 和 style 除外的非 props 屬性集合。通過 this.$attrs 獲取父作用域中所有符合條件的屬性集合,然後還要繼續傳給子元件內部的其他元件,就可以通過 v-bind="$attrs"。

場景:如果父傳子有很多值,那麼在子元件需要定義多個 props

解決:$attrs獲取子傳父中未在 props 定義的值

// 父元件
<home title="這是標題" width="80" height="80" imgUrl="imgUrl"/>

// 子元件
mounted() {
  console.log(this.$attrs) //{title: "這是標題", width: "80", height: "80", imgUrl: "imgUrl"}
},

相對應的如果子元件定義了 props,列印的值就是剔除定義的屬性。

props: {
  width: {
    type: String,
    default: ''
  }
},
mounted() {
  console.log(this.$attrs) //{title: "這是標題", height: "80", imgUrl: "imgUrl"}
},

$listeners:包含父作用域裡 .native 除外的監聽事件集合。如果還要繼續傳給子元件內部的其他元件,就可以通過 v-on="$linteners"。

場景:子元件需要呼叫父元件的方法

解決:父元件的方法可以通過 v-on="$listeners" 傳入內部元件——在建立更高層次的元件時非常有用

// 父元件
<home @change="change"/>

// 子元件
mounted() {
  console.log(this.$listeners) //即可拿到 change 事件
}

如果是孫元件要存取父元件的屬性和呼叫方法,直接一級一級傳下去就可以。

7. $children / $parent

$children:獲取到一個包含所有子元件(不包含孫子元件)的 VueComponent 物件陣列,可以直接拿到子元件中所有資料和方法等。

$parent:獲取到一個父節點的 VueComponent 物件,同樣包含父節點中所有資料和方法等

// Parent.vue
export default{
    mounted(){
        this.$children[0].someMethod() // 呼叫第一個子元件的方法
        this.$children[0].name // 獲取第一個子元件中的屬性
    }
}

// Child.vue
export default{
    mounted(){
        this.$parent.someMethod() // 呼叫父元件的方法
        this.$parent.name // 獲取父元件中的屬性
    }
}

$children$parent 並不保證順序,也不是響應式的,只能拿到一級父元件和子元件。

8. provide / inject

provide / inject 為依賴注入,主要為高階外掛/元件庫提供用例。說是不推薦直接用於應用程式程式碼中,但是在一些外掛或元件庫裡卻是被常用,所以我覺得用也沒啥,還挺好用的。

provide:可以讓我們指定想要提供給後代元件的資料或方法

inject:在任何後代元件中接收想要新增在這個元件上的資料或方法,不管元件巢狀多深都可以直接拿來用

要注意的是 provide 和 inject 傳遞的資料不是響應式的,也就是說用 inject 接收來資料後,provide 裡的資料改變了,後代元件中的資料不會改變,除非傳入的就是一個可監聽的物件

所以建議還是傳遞一些常數或者方法

// 父元件
export default{
    // 方法一 不能獲取 this.xxx,只能傳寫死的
    provide:{
        name:"RDIF",
    },
    // 方法二 可以獲取 this.xxx
    provide(){
        return {
            name:"RDIF",
            msg: this.msg // data 中的屬性
            someMethod:this.someMethod // methods 中的方法
        }
    },
    methods:{
        someMethod(){
            console.log("這是注入的方法")
        }
    }
}

// 後代元件
export default{
    inject:["name","msg","someMethod"],
    mounted(){
        console.log(this.msg) // 這裡拿到的屬性不是響應式的,如果需要拿到最新的,可以在下面的方法中返回
        this.someMethod()
    }
}

9. EventBus

EventBus 是中央事件匯流排,不管是父子元件,兄弟元件,跨層級元件等都可以使用它完成通訊操作。

  • 宣告一個全域性Vue範例變數 EventBus , 把所有的通訊資料,事件監聽都儲存到這個變數上;
  • 類似於 Vuex。但這種方式只適用於極小的專案;
  • 原理就是利用emit 並範例化一個全域性 vue 實現資料共用;
  • 可以實現平級,巢狀元件傳值,但是對應的事件名eventTarget必須是全域性唯一的;

定義方式有三種:

// 方法一
// 抽離成一個單獨的 js 檔案 Bus.js ,然後在需要的地方引入
// Bus.js
import Vue from "vue"
export default new Vue()

// 方法二 直接掛載到全域性
// main.js
import Vue from "vue"
Vue.prototype.$bus = new Vue()

// 方法三 注入到 Vue 根物件上
// main.js
import Vue from "vue"
new Vue({
    el:"#app",
    data:{
        Bus: new Vue()
    }
})

使用如下,以方法一按需引入為例:

// 在需要向外部傳送自定義事件的元件內
<template>
    <button @click="handlerClick">按鈕</button>
</template>
import Bus from "./Bus.js"
export default{
    methods:{
        handlerClick(){
            // 自定義事件名 sendMsg
            Bus.$emit("sendMsg", "這是要向外部傳送的資料")
        }
    }
}

// 在需要接收外部事件的元件內
import Bus from "./Bus.js"
export default{
    mounted(){
        // 監聽事件的觸發
        Bus.$on("sendMsg", data => {
            console.log("這是接收到的資料:", data)
        })
    },
    beforeDestroy(){
        // 取消監聽
        Bus.$off("sendMsg")
    }
}

以方法二直接掛載在全域性:

// 在 main.js
Vue.prototype.$eventBus=new Vue()

// 傳值元件
this.$eventBus.$emit('eventTarget','這是eventTarget傳過來的值')

// 接收元件
this.$eventBus.$on("eventTarget",v=>{
  console.log('eventTarget',v);//這是eventTarget傳過來的值
})

10. Vuex

  • Vuex 是狀態管理器,集中式儲存管理所有元件的狀態。
  • 適合資料共用多的專案裡面,因為如果只是簡單的通訊,使用起來會比較重。
state:定義存貯資料的倉庫 ,可通過this.$store.state 或mapState存取。
getter:獲取 store 值,可認為是 store 的計算屬性,可通過this.$store.getter 或 mapGetters存取。
mutation:同步改變 store 值,為什麼會設計成同步,因為mutation是直接改變 store 值,vue 對操作進行了記錄,如果是非同步無法追蹤改變,可通過mapMutations呼叫。
action:非同步呼叫函數執行mutation,進而改變 store 值,可通過 this.$dispatch或mapActions存取。
modules:模組,如果狀態過多,可以拆分成模組,最後在入口通過...解構引入。

這一塊內容過長,如果基礎不熟的話可以看這個Vuex,然後大致用法如下:

比如建立這樣的檔案結構

index.js 裡內容如下

import Vue from 'vue'
import Vuex from 'vuex'
import getters from './getters'
import actions from './actions'
import mutations from './mutations'
import state from './state'
import user from './modules/user'

Vue.use(Vuex)

const store = new Vuex.Store({
  modules: {
    user
  },
  getters,
  actions,
  mutations,
  state
})
export default store

然後在 main.js 引入

import Vue from "vue"
import store from "./store"
new Vue({
    el:"#app",
    store,
    render: h => h(App)
})

然後在需要的使用元件裡

import { mapGetters, mapMutations } from "vuex"
export default{
    computed:{
        // 方式一 然後通過 this.屬性名就可以用了
        ...mapGetters(["引入getters.js裡屬性1","屬性2"])
        // 方式二
        ...mapGetters("user", ["user模組裡的屬性1","屬性2"])
    },
    methods:{
        // 方式一 然後通過 this.屬性名就可以用了
        ...mapMutations(["引入mutations.js裡的方法1","方法2"])
        // 方式二
        ...mapMutations("user",["引入user模組裡的方法1","方法2"])
    }
}

// 或者也可以這樣獲取
this.$store.state.xxx
this.$store.state.user.xxx

11. $root

$root 可以拿到 App.vue 裡的資料和方法

// 父元件
mounted(){
  console.log(this.$root) //獲取根範例,最後所有元件都是掛載到根範例上
  console.log(this.$root.$children[0]) //獲取根範例的一級子元件
  console.log(this.$root.$children[0].$children[0]) //獲取根範例的二級子元件
}

12. slot

將父元件的 template 傳入子元件
分類:
A.匿名插槽(也叫預設插槽): 沒有命名,有且只有一個;

// 父元件
<todo-list> 
    <template v-slot:default>
       任意內容
       <p>我是匿名插槽 </p>
    </template>
</todo-list> 

// 子元件
<slot>我是預設值</slot>
//v-slot:default寫上感覺和具名寫法比較統一,容易理解,也可以不用寫

B.具名插槽: 相對匿名插槽元件slot標籤帶name命名的;

// 父元件
<todo-list> 
    <template v-slot:todo>
       任意內容
       <p>我是匿名插槽 </p>
    </template>
</todo-list> 

//子元件
<slot name="todo">我是預設值</slot>

C.作用域插槽: 子元件內資料可以被父頁面拿到(解決了資料只能從父頁面傳遞給子元件)

// 父元件
<todo-list>
 <template v-slot:todo="slotProps" >
   {{slotProps.user.firstName}}
 </template> 
</todo-list> 
//slotProps 可以隨意命名
//slotProps 接取的是子元件標籤slot上屬性資料的集合所有v-bind:user="user"

// 子元件
<slot name="todo" :user="user" :test="test">
    {{ user.lastName }}
 </slot> 
data() {
    return {
      user:{
        lastName:"Zhang",
        firstName:"yue"
      },
      test:[1,2,3,4]
    }
  },
// {{ user.lastName }}是預設資料  v-slot:todo 當父頁面沒有(="slotProps")

13、路由傳參

1.方案一

// 路由定義
{
  path: '/describe/:id',
  name: 'Describe',
  component: Describe
}
// 頁面傳參
this.$router.push({
  path: `/describe/${id}`,
})
// 頁面獲取
this.$route.params.id

2.方案二

// 路由定義
{
  path: '/describe',
  name: 'Describe',
  component: Describe
}
// 頁面傳參
this.$router.push({
  name: 'Describe',
  params: {
    id: id
  }
})
// 頁面獲取
this.$route.params.id

3.方案三

// 路由定義
{
  path: '/describe',
  name: 'Describe',
  component: Describe
}
// 頁面傳參
this.$router.push({
  path: '/describe',
    query: {
      id: id
  `}
)
// 頁面獲取
this.$route.query.id

4.三種方案對比

方案二引數不會拼接在路由後面,頁面重新整理引數會丟失
方案一和三引數拼接在後面,醜,而且暴露了資訊

14、Vue.observable

用法:讓一個物件可響應。Vue 內部會用它來處理 data 函數返回的物件;
返回的物件可以直接用於渲染函數和計算屬性內,並且會在發生改變時觸發相應的更新;
也可以作為最小化的跨元件狀態記憶體,用於簡單的場景。
通訊原理實質上是利用Vue.observable實現一個簡易的 vuex

// 檔案路徑 - /store/store.js
import Vue from 'vue'

export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount (count) {
    store.count = count
  }
}

//使用
<template>
    <div>
        <label for="bookNum">數 量</label>
            <button @click="setCount(count+1)">+</button>
            <span>{{count}}</span>
            <button @click="setCount(count-1)">-</button>
    </div>
</template>

<script>
import { store, mutations } from '../store/store' // Vue2.6新增API Observable

export default {
  name: 'Add',
  computed: {
    count () {
      return store.count
    }
  },
  methods: {
    setCount: mutations.setCount
  }
}
</script>

參考資料

vue.js: https://v2.cn.vuejs.org/

vuex是什麼:https://v3.vuex.vuejs.org/zh/

工作中要使用Git,看這篇文章就夠了:http://www.guosisoft.com/article/detail/410508049313861

企業數位化轉型如何做?看過來:http://www.guosisoft.com/article/detail/408745545576517

結語

如果本文對你有一點點幫助,點個贊支援一下吧,你的每一個【贊】都是我創作的最大動力 _

更多技術文章請往:http://www.guosisoft.com/article,大家一起共同交流和進步呀