vue組件通訊--注意事項及經驗總結

轉自vue組件通訊--注意事項及經驗總結

說說在 Vue.js 中如何實現組件間通訊(高級篇)

寫在前面

組件間的通訊是是實際開發中很是經常使用的一環,如何使用對項目總體設計、開發、規範都有很實際的的做用,我在項目開發中對此深有體會,總結下vue組件間通訊的幾種方式,討論下各自的使用場景html

文章對相關場景預覽vue

  • 父->子組件間的數據傳遞
  • 子->父組件間的數據傳遞
  • 兄弟組件間的數據傳遞
  • 組件深層嵌套,祖先組件與子組件間的數據傳遞

文章相關技術預覽 prop、emit、bus、vuex、路由URL、provide/inject、children/parent、attrs/inheritAttrsvuex

注:如下介紹與代碼環境:vue2.0+、vue-cli2vue-cli

父->子組件間的數據傳遞

父子組件的通訊是開發是最經常使用的也是最重要的,大家必定知道父子通訊是用prop傳遞數據的,像這樣:api

//父組件,傳遞數據
<editor :inputIndex="data" :inputName="王文健"></editor>
複製代碼

//子組件,接受數據,定義傳遞數據的類型type與默認值default
    props: {
        inputIndex: {
            type: Object, 
            default: function(){
                return {}
            }
        },
        inputName: {
            type: String,
            default: ''
        },
複製代碼
複製代碼

注意項數組

父組件傳遞數據時相似在標籤中寫了一個屬性,若是是傳遞的數據是data中的天然是要在傳遞屬性前加v-bind:,若是傳遞的是一個已知的固定值呢session

  • 字符串是靜態的可直接傳入無需在屬性前加v-bind
  • 數字,布爾,對象,數組,由於這些是js表達式而不是字符串,因此即便這些傳遞的是靜態的也須要加v-bind,把數據放到data中引用,

若是prop傳到子組件中的數據是一個對象的話,要注意傳遞的是一個對象引用,雖然父子組件看似是分離的但最後都是在同一對象下app

  • 若是prop傳到子組件的值只是做爲初始值使用,且在父組件中不會變化賦值到data中使用dom

  • 若是傳到子組件的prop的數據在父組件會被改變的,放到計算屬性中監聽變化使用。由於若是傳遞的是個對象的話,只改變下面的某個屬性子組件中是不會響應式更新的,若是子組件須要在數據變化時響應式更新那隻能放到computed中或者用watch深拷貝deep:true才能監聽到變化ide

  • 固然若是你又須要在子組件中經過prop傳遞數據的變化作些操做,那麼寫在computed中會報警告,由於計算屬性中不推薦有任何數據的改變,最好只進行計算。若是你非要進行數據的操做那麼能夠把監聽寫在watch(注意deep深拷貝)或者使用computed的getset以下圖:![計算屬性.png](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="www.w3.org/2000/svg" version="1.1" width="994" height="748"></svg>)

  • 但問題又來了,若是你傳進來的是個對象,同時你又須要在子組件中操做傳進來的這個數據,那麼在父組件中的這個數據也會改變由於你傳遞的只是個引用, 即便你把prop的數據複製到data中也是同樣的,不管如何賦值都是引用的賦值,你只能對對象作深拷貝建立一個副本才能繼續操做,你能夠用JSON的方法先轉化字符串在轉成對象更方便一點,

  • 因此在父子傳遞數據時要先考慮好數據要如何使用,不然你會遇到不少問題或子組件中修改了父組件中的數據,這是很隱蔽而且很危險的

子->父組件間的數據傳遞

在vue中子向父傳遞數據通常用**$emit**自定義事件,在父組件中監聽這個事件並在回調中寫相關邏輯

// 父組件監聽子組件定義的事件
 <editor :inputIndex="index" @editorEmit='editorEmit'></editor>
複製代碼

// 子組件須要返回數據時執行,並能夠傳遞數據
this.$emit('editorEmit', data)
複製代碼
複製代碼

那麼問題來了,我是否是真的有必要去向父組件返回這個數據,用自定義事件能夠在當子組件想傳遞數據或向子組件傳遞的數據有變化須要從新傳遞時執行,那麼另一種場景,父組件須要子組件的一個數據但子組件並不知道或者說沒有能力在父組件想要的時候給父組件,那麼這個時候就要用到組件的一個選項ref

<editor ref="editor" @editorEmit='editorEmit'></editor>

  • 父組件在標籤中定義ref屬性,在js中直接調用this.$refs.editor就是調用整個子組件,子組件的全部內容都能經過ref去調用,固然咱們並不推薦由於這會使數據看起來很是混亂,
  • 因此咱們能夠在子組件中定義一種專供父組件調用的函數,,好比咱們在這個函數中返回子組件data中某個數據,**當父組件想要獲取這個數據就直接主動調用ref執行這個函數獲取這個數據,**這樣能適應很大一部分場景,邏輯也更清晰一點
  • 另外,父向子傳遞數據也能夠用ref,有次須要在一個父組件中大量調用同一個子組件,而每次調用傳遞的prop數據都不一樣,而且傳遞數據會根據以後操做變化,這樣我須要在data中定義大量相關數據並改變它,我能夠直接用ref調用子組件函數直接把數據以參數的形式傳給子組件,邏輯一會兒清晰了
  • 若是調用基礎組件能夠在父組件中調用ref執行基礎組件中暴露的各類功能接口,好比顯示,消失等

兄弟組件間的數據傳遞

vue中兄弟組件間的通訊是很不方便的,或者說不支持的,那麼父子組件中都有什麼通訊方式呢

  • 路由URL參數

  • 在傳統開發時咱們經常把須要跨頁面傳遞的數據放到url後面,跳轉到另外頁面時直接獲取url字符串獲取想要的參數便可,在vue跨組件時同樣能夠這麼作, // router index.js 動態路由 { path:'/params/:Id', component:Params, name:Params } 複製代碼

    // 跳轉路由 跳轉路由 複製代碼

  • 在跳轉後的組件中用$route.params.id去獲取到這個id參數爲12,但這種只適合傳遞比較小的數據,數字之類的

  • Bus通訊

在組件以外定義一個bus.js做爲組件間通訊的橋樑,適用於比較小型不須要vuex又須要兄弟組件通訊的

bus.js中添加以下

import Vue from 'vue'export default new Vue
複製代碼
複製代碼

組件中調用bus.js經過自定義事件傳遞數據

import Bus from './bus.js'export default { 
      methods: {
         bus () {
            Bus.$emit('msg', '我要傳給兄弟組件們')
         }
      }
  }
複製代碼
複製代碼

兄弟組件中監聽事件接受數據

import Bus from './bus.js'export default {
        mounted() {
           Bus.$on('msg', (e) => {
             console.log(e)
           })
         }
       }
複製代碼
複製代碼

注:以上兩種使用場景並不高因此只是簡略提一下,這兩點都是好久之前寫過,以上例子網上直接蒐集而來若有錯誤,指正

  • Vuex集中狀態管理 vuex是vue的集中狀態管理工具,對於大型應用統一集中管理數據,很方便,在此對vuex的用法並不過多介紹只是提一下使用過程當中遇到的問題

規範:對於多人開發的大型應用規範的制定是相當重要的,對於全部人都會接觸到的vuex對其修改數據調用數據都應有一個明確嚴格的使用規範

  1. vuex分模塊:項目不一樣模塊間維護各自的vuex數據
  2. 限制調用:只容許action操做數據,getters獲取數據,使用mapGetters,mapActions輔助函數調用數據![vuex.png](data:image/svg+xml;utf8,<?xml version="1.0"?><svg xmlns="www.w3.org/2000/svg" version="1.1" width="1136" height="1280"></svg>)

對於vuex的使用場景也有一些爭論,有人認爲正常組件之間就是要用父子組件傳值的方式,即便子組件須要使vuex中的數據也應該由父組件獲取再傳到子組件中,但有的時候組件間嵌套很深,只容許父組件獲取數據並非一個方便的方法,因此對於祖先元組件與子組件傳值又有了新問題,vue官網也有一些方法解決,以下

祖先組件與子組件間的數據傳遞

provide/inject 除了正常的父子組件傳值外,vue也提供了provide/inject

這對選項須要一塊兒使用,以容許一個祖先組件向其全部子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效

官網實例

// 父級組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
}
複製代碼
複製代碼
  • provide 選項應該是一個對象或返回一個對象的函數。該對象包含可注入其子孫的屬性。

  • 一個字符串數組,或 一個對象,對象的 key 是本地的綁定名,value 是:

  • 在可用的注入內容中搜索用的 key (字符串或 Symbol),或 一個對象,該對象的:

  • from 屬性是在可用的注入內容中搜索用的 key (字符串或 Symbol)

  • default 屬性是降級狀況下使用的 value

提示:provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。 具體細節移步vue相關介紹cn.vuejs.org/v2/api/#pro…

provide/inject還未在項目中應用過,後面會作嘗試


補充 $attrs/inheritAttrs

經小夥伴們提醒補充$attrs的使用

場景:祖先組件與子組件傳值

  • 若是是props的話,就必須在子組件與祖先組件之間每一個組件都要prop接受這個數據,再傳到下一層子組件,這就很麻煩,耦合深程序臃腫
  • 若是用vuex確實顯得有點小題大作了,因此用$attrs直接去獲取祖先數據也不錯

包含了父做用域中不做爲 prop 被識別 (且獲取) 的特性綁定 (class 和 style 除外)。當一個組件沒有聲明任何 prop 時,這裏會包含全部父做用域的綁定 (class 和 style 除外),而且能夠經過 v-bind="$attrs" 傳入內部組件——在建立高級別的組件時很是有用。

以上是官網對$attrs的解釋,我剛看我也是一臉懵逼,回去試了一下其實並不難,並且比較適用組件深層嵌套場景下,祖先組件向子組件傳值的問題

意思就是父組件傳向子組件傳的,子組件不prop接受的數據都會放在attrs中,子組件直接用this.attrs獲取就能夠了。如過從父->孫傳,就在子組件中添加v-bind='$attrs',就把父組件傳來的子組件沒props接收的數據所有傳到孫組件,具體看如下代碼

使用:

祖先組件

// 祖先組件
// 在祖先組件中直接傳入output和input
<template>
  <div>
    <child1 :output='output' :input="input"></child1>
  </div>
</template>
<script>
import child1 from './child1.vue'export default {
  components: {
    child1
  },
  data () {
    return {
      input: 'jijijijjijiji',
      output: {
        name: '王文健',
        age: '18'
      }
    }
  }
</script>
複製代碼
複製代碼

子組件

<template>
  <div
    <h1>{{input}}</h1>
    <child2 :child="child" v-bind='$attrs'></child2>
  </div>
</template>
<script>
import child2 from './child2.vue'export default {
  components: {
    child2
  },
  props: {
    input: [String]
  },
  data () {
    return {
      child: 'child1child1child1child1s'
    }
  },
// 默認爲true,若是傳入的屬性子組件沒有prop接受,就會以字符串的形式出現爲標籤屬性
// 設爲false,在dom中就看不到這些屬性,試一下就知道了
  inheritAttrs: false,
  created () {
    // 在子組件中打印的$attrs就是父組件傳入的值,刨去style,class,和子組件中已props的屬性
    console.log(this.$attrs)  // 打印output
  }
}
</script>

複製代碼
複製代碼

孫組件

<template>
  <div>
    {{$attrs.output.name}}
  </div>
</template>
<script>
export default {
  created () {
    // 打印output和child
    console.log(this.$attrs)
  }
}
</script>

複製代碼
複製代碼

看起來仍是挺好用的,還沒在具體項目中用過,相信不久會用到的,若是還有什麼問題歡迎留言

children/parent

固然你能夠直接用children/parent獲取當前組件的子組件實例或父組件實例(若是有的話),也能對其作些操做,不過並不推薦這麼作

你還能夠放到localStorage,sessionStorage,cooikes之類的存在本地固然也能作到組件間的通訊


以前說過,可使用 props 將數據從父組件傳遞給子組件。其實還有其它種的通訊方式,下面咱們一一娓娓道來。

1 自定義事件

經過自定義事件,咱們能夠把數據從子組件傳輸回父組件。子組件經過 $emit() 來觸發事件,而父組件經過 $on() 來監聽事件,這是典型的觀察者模式。

html:

<div id="app">
    <p>總數:{{total}}</p>
    <deniro-component @increase="setTotal"
                      @reduce="setTotal"
    ></deniro-component>
</div>
複製代碼
複製代碼

js:

Vue.component('deniro-component', {
	template: '\
	<div>\
	<button @click="increase">+1</button>\
	<button @click="reduce">-1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		increase: function () {
			this.counter++;
			this.$emit('increase', this.counter);
		},
		reduce: function () {
			this.counter--;
			this.$emit('reduce', this.counter);
		}
	}
});

var app = new Vue({
	el: '#app',
	data: {
		total: 0
	},
	methods: {
		setTotal: function (total) {
			this.total = total;
		}
	}
});
複製代碼
複製代碼

效果:

示例中有兩個按鈕,分別實現加 1 與減 1 操做。點擊按鈕後,執行組件中定義的 increase 或 reduce 方法,在方法內部,使用 $emit 把值傳遞迴父組件。 $emit 方法的第一個參數是使用組件時定義的事件名,示例中是 @increase@reduce

<deniro-component @increase="setTotal"
				  @reduce="setTotal"
></deniro-component>
複製代碼
複製代碼

這兩個事件又綁定了 setTotal 方法,該方法修改了 total 值。 $emit 方法的其它參數是須要回傳給父組件的參數。

也可使用 v-on.native 來監聽原生事件,好比這裏監聽組件的點擊事件:

html:

<div id="app">
    ...
    <deniro-component ...
                      @click.native="click"
    ></deniro-component>
</div>
複製代碼
複製代碼

js:

...
var app = new Vue({
	el: '#app',
	data: {
		total: 0
	},
	methods: {
		...
		click: function () {
			console.log("原生點擊事件");
		}
	}
});
複製代碼
複製代碼

這樣,點擊按鈕後,就能夠捕獲原生的點擊事件啦O(∩_∩)O~

**注意:**這裏監聽的是這個組件根元素的原生點擊事件。

2 v-model 方式

也可使用 v-model 方式來直接綁定父組件變量,把數據從子組件傳回父組件。

html:

<div id="app2">
    <p>總數:{{total}}</p>
    <deniro-component2 v-model="total"
    ></deniro-component2>
</div>
複製代碼
複製代碼

js:

Vue.component('deniro-component2', {
	template: '\
	<div>\
	<button @click="click">+1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		click: function () {
			this.counter++;
			this.$emit('input', this.counter);
		}
	}
});

var app2 = new Vue({
	el: '#app2',
	data: {
		total: 0
	}
});
複製代碼
複製代碼

效果:

咱們使用 v-model="total" 直接綁定變量 total。接着在子組件中,在 $emit 方法傳入事件名 input,這樣 Vue.js 就會自動找到 `v-model 綁定的變量啦O(∩_∩)O~

咱們也可使用自定義事件來實現上述示例——

html:

<div id="app3">
    <p>總數:{{total}}</p>
    <deniro-component3 @input="setTotal"
    ></deniro-component3>
</div>

複製代碼
複製代碼

js:

Vue.component('deniro-component3', {
	template: '\
	<div>\
	<button @click="click">+1</button>\
	</div>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		click: function () {
			this.counter++;
			this.$emit('input', this.counter);
		}
	}
});

var app3 = new Vue({
	el: '#app3',
	data: {
		total: 0
	},
	methods: {
		setTotal: function (total) {
			this.total = total;
		}
	}
});
複製代碼
複製代碼

效果與上例相同。

咱們還能夠在自定義的表單輸入組件中利用 v-model,實現數據雙向綁定:

html:

<div id="app4">
    <p>總數:{{total}}</p>
    <deniro-component4 v-model="total"
    ></deniro-component4>
    <button @click="increase">+1</button>
</div>
複製代碼
複製代碼

js:

Vue.component('deniro-component4', {
	props: ['value'],
	template: '<input :value="value" @input="update">+1</input>',
	data: function () {
		return {
			counter: 0
		}
	},
	methods: {
		update: function (event) {
			this.$emit('input', event.target.value);
		}
	}
});

var app4 = new Vue({
	el: '#app4',
	data: {
		total: 0
	},
	methods: {
		increase: function () {
			this.total++;
		}
	}
});
複製代碼
複製代碼

效果:

這裏咱們首先利用 v-model,在自定義組件中綁定了 total 變量。而後在組件內部,定義了 props 爲 ['value']注意這裏必須爲 value,才能接收綁定的 total 變量。接着在組件模板中把接收到的 value 值(即 total 變量值),做爲 <input> 元素的初始值,並綁定 input 事件。下一步,在 input 事件中,經過 this.$emit('input', event.target.value) 把 total 值傳回父組件的 <button @click="increase">+1</button>。最後在 increase 方法中,遞增 total 值。

這個示例,咱們綜合使用了 props 、v-model和自定義事件,實現了數據的雙向綁定。

總的來講,一個具備雙向綁定的 v-model 組件具備如下特徵:

  1. 使用 props 接收父組件的 value。
  2. 子組件中擁有能夠更新 value 的 HTML 元素,當更新 value 時,觸發 input 事件。事件內部使用 $emit 將新的 value 值回傳給父組件。

3 非父子組件

非父子組件指的是兄弟組件或者跨多級組件。

3.1 中央事件總線

咱們能夠建立一個空的 Vue 實例做爲中央事件總線,實現非父子組件之間的通訊。

html:

<div id="app5">
    <p>監聽子組件消息:{{message}}</p>
    <deniro-component5></deniro-component5>
</div>
複製代碼
複製代碼

js:

var bus = new Vue();
Vue.component('deniro-component5', {
	template: '<button @click="sendMessage">發送消息</button>',
	methods: {
		sendMessage: function () {
			bus.$emit('on-message', '來自於 deniro-component5 的消息');
		}
	}
});
var app5 = new Vue({
	el: "#app5",
	data: {
		message: ''
	},
	mounted: function () {
		var that = this;

		bus.$on('on-message', function (message) {
			that.message = message;
		})
	}
});
複製代碼
複製代碼

注意: 由於 bus.$on() 中的函數,this 指向的是自己,因此咱們必須在外層定義一個 that,讓它引用 mounted 對象。

效果:

首先建立了一個空的 Vue 實例做爲中央事件總線。而後在定義的子組件綁定的 click 事件中,經過 bus.$emit() 發送消息。接着在初始化 app 實例的 mounted 函數時,使用 bus.$on() 方法監聽消息。

這種方式能夠實現組件間任意通訊。咱們還能夠擴展 bus 實例,爲它添加 data、methods、computed 等屬性,這些都是公共屬性,能夠共用。因此在此能夠放置須要共享的信息,好比用戶登錄暱稱等。使用時只須要初始化一次 bus 便可,因此在單頁面富客戶端中應用普遍。

若是項目較大,那麼可使用具備狀態管理的 vuex 哦O(∩_∩)O~

3.2 父子鏈

子組件可使用 this.$parent 來訪問父組件實例;而父組件可使用 this.$children 來訪問它的全部子組件實例。這些方法能夠遞歸向上或向下,直到根實例或者葉子實例。

html:

<div id="app6">
    <p>消息:{{message}}</p>
    <deniro-component6></deniro-component6>
</div>
複製代碼
複製代碼

js:

Vue.component('deniro-component6', {
	template: '<button @click="sendMessage">發送消息</button>',
	methods: {
		sendMessage: function () {
			//經過父鏈找到父組件,修改相應的變量
			this.$parent.message='來自於 deniro-component6 的消息';
		}
	}
});
var app6 = new Vue({
	el: "#app6",
	data: {
		message: ''
	}
});
複製代碼
複製代碼

效果:

**注意:**只有在萬不得已的狀況下,才使用父子鏈,實現組件間任意通訊。由於這樣作,會讓兩個組件之間緊耦合,代碼變得難理解與維護。若是隻是父子組件之間的通訊,儘可能採用 props 與自定義事件 $emit 來實現。

3.3 子組件索引

若是一個組件的子組件較多且是動態渲染的場景,使用 this.$children 來遍歷這些子組件較麻煩。這時就可使用 ref 來爲子組件指定索引名稱,方便後續查找。

html:

<div id="app7">
    <button @click="getChild">獲取子組件實例</button>
    <deniro-component7 ref="child"></deniro-component7>
</div>
複製代碼
複製代碼

js:

Vue.component('deniro-component7', {
	template: '<div>deniro-component7</div>',
	data: function () {
		return {
			message: '登錄不到兩週,InSight探測器意外捕捉到火星的風聲'
		}
	}
});
var app7 = new Vue({
	el: "#app7",
	methods: {
		getChild: function () {
			//使用 $refs 來訪問組件實例
			console.log(this.$refs.child.message);
		}
	}
});
複製代碼
複製代碼

輸出結果:

登錄不到兩週,InSight探測器意外捕捉到火星的風聲

注意:$refs 只在組件渲染完成後纔會被賦值,並且它是非響應式的。因此只有在萬不得已的狀況下才使用它。

本文示例代碼


總結以下: 通訊方式通訊方向props【推薦】父組件到子組件自定義事件 $emit【推薦】子組件到父組件中央事件總線【推薦】組件間任意通訊父子鏈組件間任意通訊子組件索引父組件到子組件

相關文章
相關標籤/搜索