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

前言

vue是數據驅動視圖更新的框架, 我們平時開發,都會把頁面不同模塊拆分成一個一個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。

props與$emit

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。但這種方式只適用於極小的項目;
  • 原理就是利用on和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,然後大致用法如下:

比如創建這樣的文件結構

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,大家一起共同交流和進步呀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章