Vue組件通訊事件總結

在Vue的項目開發裏面,組件之間的通訊實在是太常見了,雖然說Vue已經出了好久了,但我接觸它的時間仍不是不少,趨於業務開發,有時候會踩到一些坑,也是初級開發者很容易遇到的問題,固然網上也都有不少解決方案的文章,不過每次一遇到問題就百度一下的習慣並無讓本身系統的理解Vue裏面的通訊機制。好比.sync這個修飾符,使用ElementUI的Dialog組件常見到,但一直是拿來即用,沒有思索過具體用處,遂總結整理一下在Vue中組件之間的通訊機制,方便往後開發中按照場景使用更合適的API,而不是隻會$emit$on這一種用法。html

本篇文章主要參考學習了掘金的一篇文章,指路➡️ vue組件通訊全揭祕 , 感受講的很詳細,不少地方給我一種恍然大悟的感受,很是感謝。vue

data 和 props (同步變化)

在vue中,咱們最多見的恐怕莫過於data了,vue經過{{}}綁定動態數據,當data的值發生變化時,能夠同步的更新到視圖中。vuex

而props經常使用於數據傳遞,好比子組件想要使用父組件中的數據,就能夠經過props接收父組件的值。而父組件使用v-bind來把數據傳遞給子組件。當父組件中的數據發生改變時,子組件中對應的視圖層也會相應的更新。數組

以上二者裏的每一個數據都是行爲操做須要的數據或者模板 view 須要渲染的數據,一旦其中一個屬性發生變化,則全部關聯的行爲操做和數據渲染的模板上的數據同一時間進行同步變化。瀏覽器

// 父組件A
<template>
   <div>
      <p>test: {{test}}</p>
      <p>msgData: {{test}}</p>
      <ComponentB :msg='msgData'></ComponentB>
   </div>
</template>
<script>
import ComponentB from './ComponentB.vue'
   export default {
     data () {
       return  {
          test: '我是測試數據',
          msgData: '我是傳遞給子組件的數據'
       }
     },
     components : {
       ComponentB
     }
   }
</script>


// 子組件B
<template>
   <div>
      <p>msgData: {{msgData}}</p>
   </div>
</template>
<script>
   export default {
      name: 'ComponentB',
      props: [ 'msgData'],
   }
</script>

複製代碼

data 和 props 的不一樣

  • data在任何狀況下改變數據類型和數據結構,都能同步反應到view層。
  • 而props一旦初始化後,在數據傳遞時不能改變它的數據類型;由於Vue時單向數據流,經過父組件傳遞的數據,在子組件中不能修改,只能改變傳遞源中的數據。

雖然都會影響視圖層的數據,不過props跟data的使用仍是有些區別的。下面就來講說我在代碼中遇到過的有關props的坑(也多是常見的錯誤):bash

Props踩過的坑數據結構

  1. 改變props數據報錯:

在子組件中不能夠直接修改props中的數據,這也正是vue中單向數據流的特性,父組件向下傳遞的數據,若是父組件發生改變,會同步的反應在子組件中;但子組件的數據卻不會同步影響上一層,就像水同樣,只能往下走。(固然能夠經過其餘方式來影響父組件的數據,好比事件)dom

  1. 若是實在是想要改動props傳遞過來的數據怎麼辦?

雖然說props的數據是不能顯式的直接改變,但咱們能夠經過間接的方式來修改數據。函數

  • 一、利用data的動態性,咱們能夠將props的數據轉存到data中,這樣就能夠經過修改data中的數據來更改對應的視圖層啦
props: ['msg'],
data() {
  return { 
    myMsg: this.msg 
  }
}
複製代碼
  • 二、能夠轉存到計算屬性computed中
props:['msg']
computed : {
   myMsg () {
       return this.msg;
   }
}
複製代碼

不過由於對象和數組是引用類型,指向同一個內存空間,因此不要經過computed來對父組件傳遞來的引用類型數據進行計算過濾,改變數據會影響到父組件的狀態。post

props對象傳遞簡寫

  • 當父組件傳遞給子組件的數據數量不少,能夠經過一個對象傳遞:
<!--父組件:-->

<!--html部分: -->
<demo v-bind='msg'></demo>

<!--js部分:-->
data () {
    return {
        msg : {a:1,b:2}
    }
}


<!--子組件:-->
props: ['a','b']


<!--父組件傳遞的過程至關於以下:-->
<demo :a='a' :b='b' ></demo>
複製代碼

$emit$onv-on

上面的props中說過,子組件不能直接改變父組件傳遞進來的數據,除了經過data或computed轉存的間接方式來改變傳遞的數據外,還有另外一種方式,就是$emit事件。子組件用通訊的方式來告知父組件進行數據的更新。

  • $on(eventName, callback) 監聽事件, 第二個參數是回調函數,回調函數的參數爲$emit傳遞的數據內容
  • $emit(eventName, [...arg]) 觸發事件, 第二個參數會傳遞給on監聽器的回調函數
  • v-on則是使用在父組件標籤中的,能夠對其子組件的$emit監聽

.sync 雙向綁定

固然,v-model也是用於雙向綁定的,不過由於太常見了,暫時沒有作進一步的分析。

  • .sync的功能是:當一個子組件改變了一個 prop 的值時,這個變化也會同步到父組件中所綁定。
  • 使用方式: <comp :foo.sync="bar"></comp>
  • 至關於:<comp :foo="bar" @update:foo="val => bar = val"></comp>
  • 當子組件須要更新 foo 的值時,它須要顯式地觸發一個更新事件: this.$emit('update:foo', newValue)
<!--常見的如餓了麼組件中的:current-page.sync-->
<el-pagination 
layout="prev, pager, next" 
:total="meta.total" 
@current-change="load" 
:current-page.sync="meta.current_page" 
background>
</el-pagination>
    
<!--js部分:-->
props: {
    meta: {
      type: Object,
      required: true,
    },
  },
複製代碼

再如,餓了麼組件Dialog:

<el-dialog
  title="提示"
  :visible.sync="dialogVisible"
  width="30%"
  :before-close="handleClose">
  <span>這是一段信息</span>
  <span slot="footer" class="dialog-footer">
    <el-button @click="dialogVisible = false">取 消</el-button>
    <el-button type="primary" @click="dialogVisible = false">確 定</el-button>
  </span>
</el-dialog>
複製代碼

$attrs,$listeners, 深層次數據傳遞(Vue v2.4及以上使用)

當數據層次變多後,已經不只包括父子組件這麼簡單的關係了,可能會有第三層、第四層組件,複雜的狀況下能夠用vuex,但簡單狀況下只是偶爾有多層組件關係時,能夠選擇使用$attrs,這種方式 組件封裝用的較多

先來解釋一下這兩個屬性:

  • $attrs: 包含了父做用域中不做爲 props 被識別 (且獲取) 的特性綁定 ( class 和 style 除外)。當一個組件沒有聲明任何 props 時,這裏會包含全部父做用域的綁定 ( class 和 style 除外),而且能夠經過 v-bind="$attrs"傳入內部組件 —— 在建立高級別的組件時很是有用。
  • $listeners: 包含了父做用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它能夠經過 v-on="$listeners" 傳入內部組件 —— 在建立更高層次的組件時很是有用

事實上,你能夠把 $attrs$listeners比做兩個集合,其中 $attrs 是一個屬性集合,而 $listeners 是一個事件集合,二者都是 以對象的形式來保存數據 。

好比咱們有三個組件A,B,C,分別是父組件、子組件、孫組件,若是想從A組件傳遞數據msg給C組件:

常規作法是一層一層使用props來傳遞,但實際上B並不須要A傳遞的這個msg,除了這種思路外,還有一種寫法,

A組件:

<template>
  <div class="home">
    這裏是首頁
    <ChildB
    :one="one"
    :two="two"
    @clickOne.native="triggerOne"
    @clickTwo="triggerTwo"
    ></ChildB>
  </div>
</template>

<script>
import ChildB from '../components/ChildB.vue';

export default {
  name: 'home',
  components: {
    ChildB,
  },
  data() {
    return {
      one: 'data 1',
      two: 'data 2',
    };
  },
  methods: {
    triggerOne() {
      console.log('triggerOne');
    },
    triggerTwo() {
      console.log('triggerTwo');
    },
  },
};
</script>

複製代碼

B組件:

<template>
  <div>
    B 組件
    {{one}}:
    <ChildC></ChildC>
  </div>
</template>

<script>
import ChildC from './ChildC.vue';

export default {
  components: {
    ChildC,
  },
  props: ['one'],
  created() {
    console.log('$attrsB:', this.$attrs);
    console.log('$listenersB:', this.$listeners);
  },
};
</script>
複製代碼

C組件:

<template>
  <div>
    我是 C 組件
  </div>
</template>

<script>

export default {
  created() {
    console.log('$attrsC:', this.$attrs);
    console.log('$listenersC:', this.$listeners);
  },
};
</script>

複製代碼

此時控制檯打印出:

$attrsB: {two: "data 2"}

$listenersB: {clickTwo: ƒ invoker()}

$attrsC:{}

$listenersC: {}

複製代碼

這時候B組件能夠經過this.$attrsthis.$listeners,直接訪問A組件的屬性和事件,注意,這個屬性是非props中的才能經過這種方式訪問,而帶.native修飾器(監聽組件根元素的原生事件)的事件也一樣訪問不到。

注意,此時C組件使用this.$attrsthis.$listeners仍是訪問不到A組件的屬性和事件的。

咱們能夠經過在B組件中調用C組件的過程當中使用v-on="$listeners" 一級級地往下傳遞,此時C組件中就能夠訪問到A組件的事件了

<!--B組件:-->
<ChildC v-on="$listeners"></ChildC>


<!--C組件-->
<template>
  <div>
    我是 C 組件
  </div>
</template>

<script>

export default {
  created() {
    console.log('$attrsC:', this.$attrs);
    console.log('$listenersC:', this.$listeners);
    this.$listeners.clickTwo();
  },
};
</script>

複製代碼

輸出:

$attrsC: {}
$listenersC: {clickTwo: ƒ}
triggerTwo
複製代碼

還記得咱們上面的two屬性嘛?咱們給B組件傳遞的props屬性只有one沒有two,那這個時候two屬性去哪裏了呢?

  • 組件編譯以後會把非 props 屬性當成原始屬性對待,從而添加到DOM元素(HTML標籤上),如這裏的two屬性,在編譯成html後,two變成了一個div標籤上的屬性: <div two="data 2">

A組件到C組件的數據傳遞 上面簡單介紹了下$attrs,和$listeners的基礎用法,回到三個組件傳遞數據這一點上,咱們看一下多層級組件到底怎麼藉助$attrs,和$listeners來實現數據通訊的。

A組件不變,

B組件:

<template>
  <div>
    B 組件
    <p>props: {{one}} </p>
    <p>$attrs: {{$attrs}} </p>
    <p>$listeners: {{$listeners}}  </p>

    <ChildC v-on="$listeners" v-bind="$attrs"></ChildC>
  </div>
</template>

<script>
import ChildC from './ChildC.vue';

export default {
  components: {
    ChildC,
  },
  inheritAttrs: false,
  props: ['one'],
  created() {
    console.log('ComponentB', this.$attrs, this.$listeners);
  },
};
</script>

複製代碼

C組件:

<template>
  <div>
    我是 C 組件
    <p>props: {{two}} </p>
    <p>$attrs: {{$attrs}} </p>
    <p>$listeners: {{$listeners}}  </p>
  </div>
</template>

<script>

export default {
  props: ['two'],
  created() {
    console.log('ComponentC', this.$attrs, this.$listeners);
  },
};
</script>

複製代碼

此時控制檯上打印出:

ComponentB {two: "data 2"} {clickTwo: ƒ}
ComponentC {} {clickTwo: ƒ}
複製代碼

能夠發現,C組件能夠直接經過props來繼承A組件中的two屬性,不過這是在B組件直接往下傳遞v-bind="$attrs"的前提下。

代碼裏有一段 inheritAttrs: false, 這就是爲了禁止非props的屬性添加到DOM元素上,如今打開瀏覽器的調試窗口,就不會看見two屬性了

$children / $parent 方式

這種方式適用於木偶組件,何爲木偶組件?就是爲了業務頁面進行拆分而造成的組件模式。好比一個頁面,能夠分多個模塊,而每個模塊與其他頁面並無公用性,只是純粹拆分。

這種狀況下的組件明確的知道本身的父組件是哪個,且不須要複用,就能夠經過 $children / $parent 方式來進行通訊了。

$parent

好比A組件種有one和two兩個屬性,咱們不須要經過v-bind來傳遞,就能夠直接在A的子組件中經過this.$parent.onethis.$parent.two 來獲取。而且還能夠改變父組件的數據:this.$parent.one = '父組件one被改了',

一樣事件也能夠經過這種方式調用,父組件定義parentMethods()方法,

子組件調用:

this.$parent.parentMethods()
複製代碼

$children

$children$parent 相反,是父組件拿到子組件的實例,須要注意的是,$children是以一個數組的形式包裹。

this.$children.forEach(item => {
    console.log(item);
})
複製代碼

ref / refs

ref:若是在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;若是用在子組件上,引用就指向組件實例,能夠經過實例直接調用組件的方法或訪問數據。獲取 Dom 元素就是爲了進行一些 Dom 操做,須要注意的一點就是,要在 Dom 加載完成後使用,不然可能獲取不到。

使用場景:

  • 父元素的某個狀態改變,須要子組件進行 http 請求更新數據

ref的三種使用方式:

一、ref 加在普通的元素上,用this.ref.name 獲取到的是dom元素

<input type="text" ref="input1"/>

<!--JS部分改變數據-->
this.$refs.input1.value ="test"; //this.$refs.input1 減小獲取dom節點的消耗
複製代碼

二、ref 加在子組件上,用this.ref.name 獲取到的是組件實例,可使用組件的全部方法。

<!--父組件:-->
<child ref="childComponent"></child>
<!--JS調用:-->
this.$refs.childComponent.getLocalData()

<!--子組件定義方法:-->
getLocalData(){
    // ...
}

複製代碼

三、如何利用 v-for 和 ref 獲取一組數組或者dom 節點

<ul v-for="(item, key) in list" :key="key">
    <li ref="item">姓名:{{item.name}}, 年齡: {{item.age}}</li>
</ul>
    
<!--JS部分   -->
data() {
    return {
        list: [
        { name: 'armor', age: 24 },
        { name: 'abtion', age: 23 },
        { name: 'lili', age: 12 },
        { name: 'yangyang', age: 29 },
      ],
    }
},
created() {
    console.log('refs: ', this.$refs);
}

複製代碼

這裏打印的結果是:

refs:  {
    item: [
    0: li,
    1: li,
    2: li,
    3: li
    ]
}
複製代碼

每個li就是對應的DOM元素,而後就能夠對其進行操做了。

中央事件eventBus (非父子關係的同級組件)

如First組件與同級組件Second相互通訊,這兩個沒有共同父組件。

  1. 定義一箇中央事件實例:
import Vue from 'vue'

export default new Vue()
複製代碼
  1. First和Second組件都須要引入這個中央事件實例
import Bus from './bus.js'

<!--First組件觸發:-->
Bus.$emit('fromFirst', '來自A的組件')

<!--B組件監聽-->
Bus.$on('fromFirst', ( Amsg )=> {
    this.Bmsg = Amsg
    console.log('同級組件交互成功')
})
複製代碼

小結

其實Vue的通訊事件不止上面幾種,還有vuex中的數據交互,可是vuex比較複雜,通常不是很複雜的業務其實用不上vuex,用上面幾種方式實現組件之間的通訊足矣。

文章參考:

vue組件通訊全揭祕

相關文章
相關標籤/搜索