Vue組件的通信方式大致有這11(12)種
- 常用的Props
- $attrs & $listeners
- provide & inject
- $parent & $children
- $root
- 自定義事件的 $emit & $on
- sync語(yǔ)法糖(廢棄的修飾符 轉(zhuǎn) 語(yǔ)法糖)
- vModel語(yǔ)法糖
- 粗暴的$refs獲取子組件
- EventBus
- Vuex
- 廢棄的$boradcast & $dispatch
我只使用過(guò)前11種,最后一個(gè)因?yàn)橐呀?jīng)廢棄,也不作為語(yǔ)法糖,所以大家有興趣可以單獨(dú)去了解一下
1. props的使用
props是最基礎(chǔ)的組件單項(xiàng)數(shù)據(jù)流通信,一般代碼如下:
// 創(chuàng)建全局的tips組件
Vue.component('tips',{
props:['value'],
render: function (h) {
return (
<div class='tips-cover'>
<div class="tips-msg">{this.value}</div>
</div>
)
}
})
// 父組件中引入子組件
<tips v-if="show_tips" value="這是個(gè)基本的彈層"></tips>
// ...
export default {
// ...
mixins: [tipsMixin],
//...
}
// ...tipsMixin中的內(nèi)容
export default {
data () {
return {
show_tips: false
}
},
methods: {
showTips () {
console.log(this)
this.show_tips = true
setTimeout(() => {
this.show_tips = false
},3000)
}
}
}
如果只使用props往往會(huì)存在一個(gè)問(wèn)題,因?yàn)閜rops是單向數(shù)據(jù)流,也就是數(shù)據(jù)只能由父到子,本身不提供子組件直接改變父組件的方式,只能父組件把自己的方法傳給子組件,再在子組件中回調(diào)父組件的方法,舉個(gè)簡(jiǎn)單的例子,如果我寫(xiě)一個(gè)名為tips的彈層提示組件,如果我把控制組件顯示邏輯的變量寫(xiě)在了子組件里,父組件如何去改變子組件的變量值來(lái)顯示或隱藏子組件?如果不借助其他的方法似乎不能吧?所以只能把控制顯示的變量和相關(guān)方法都寫(xiě)在父組件里,每個(gè)父組件都mixin相關(guān)的data和methods。感覺(jué)這樣寫(xiě)比較死板,比如我要維護(hù)這個(gè)組件的時(shí)候,需要改對(duì)應(yīng)組件的vue/js文件,還要去修改父組件的mixin.js。
2. $attrs & $listeners
$attrs & $listeners 的初始化發(fā)生在生命周期 beforeCreate 之前的 initRender 函數(shù)中,使用 defineReactive(defineProperty) 將$attrs和$listener綁到了vm(vue對(duì)象)上,如果父組件傳遞的參數(shù)發(fā)生變動(dòng),會(huì)觸發(fā)updateChildComponent, 并對(duì)值進(jìn)行更新
vm.$attrs = parentVnode.data.attrs || emptyObject;<br>
vm.$listeners = listeners || emptyObject;
$attrs表示父組件傳遞下來(lái)的props的集合
$listeners表示父組件傳遞下來(lái)的invoker函數(shù)的集合
舉個(gè)例子:
// 父組件中引用子組件
<attrAndListenersCom @setGrandData="setGrandData" :fatherdata='fa_data'></attrAndListenersCom>
在子組件中$attrs就是{fatherdata: 父組件中fa_data的值}
在子組件中$listeners就是 {setGrandData: ?}
然后子組件可以使用如下的方法,將父組件的參數(shù)繼續(xù)傳遞給自己的子組件
從而實(shí)現(xiàn)了父組件對(duì)孫子組件之間的數(shù)據(jù)傳遞
// 子組件中再引用其他子組件
<attrAndListenersComCom v-bind="$attrs" v-on="$listeners"></attrAndListenersComCom>
孫子組件簡(jiǎn)易代碼如下
<template>
<div>孫子引用父組件的變量:{{$attrs.fatherdata}}</div>
<div class="btn" @click='test'>點(diǎn)我觸發(fā)一些操作</div>
</template>
<script>
methods: {
test () {
this.$emit('setGrandData', '孫子組件來(lái)了!')
}
}
</script>
點(diǎn)擊按鈕,可以改變?nèi)齻€(gè)組件中,對(duì)fa_data的引用,即父組件的fa_data,子組件的$attrs.fatherdata,和孫子組件中的$attrs.fatherdata
值得注意的是,$attrs中不會(huì)出現(xiàn)被props引用過(guò)的值,也就是如果子組件的props引用了fatherdata,那他的$attrs就是空的。這個(gè)過(guò)程發(fā)生在createComponent(組件創(chuàng)建)中,會(huì)調(diào)用extractPropsFromVNodeData函數(shù),其內(nèi)部的checkProp函數(shù)會(huì)刪除$attrs中在props中出現(xiàn)的變量。
還有就是:$attrs的賦值過(guò)程發(fā)生在updateChildComponent中,是一層一層往下傳遞的,所以你在層級(jí)較高的組件中對(duì)$attrs進(jìn)行watch,watch的回調(diào)經(jīng)常會(huì)被觸發(fā)多次。但這并不是因?yàn)槊恳粚佣紩?huì)響應(yīng)一次變動(dòng),而是有點(diǎn)類(lèi)似ReactHook中 useMemo 記憶組件的感覺(jué):父組件有2個(gè)子組件a和b,對(duì)a中參數(shù)的改變有可能會(huì)觸發(fā)b的重新渲染。個(gè)人理解這里也是一個(gè)道理,你的各種異步操作對(duì)父組件data的操作,觸發(fā)了updateChildComponent,最后都會(huì)響應(yīng)到深層子組件/$attrs的Watcher上。
個(gè)人對(duì) $attrs 使用場(chǎng)景的理解是:參數(shù)的逐層傳遞
3. provide & inject
inject的初始化發(fā)生在beforeCreate與created之間,先于provide的初始化
callHook(vm, 'beforeCreate');
initInjections(vm); // 初始化inject
initState(vm);
initProvide(vm); // 初始化provide
callHook(vm, 'created');
inject初始化相關(guān)源碼:
function initInjections (vm) {
/**
initInjections的功能就是把inject掛載在vm上
**/
var result = resolveInject(vm.$options.inject, vm);
if (result) {
toggleObserving(false);
Object.keys(result).forEach(function (key) {
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], function () {
...
});
} else {
defineReactive(vm, key, result[key]);
}
});
toggleObserving(true);
}
}
/**
resolveInject的功能就是遍歷所有的父組件,拿到他們的provide
**/
function resolveInject (inject, vm) {
if (inject) {
var result = Object.create(null);
var keys = hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject);
for (var i = 0; i < keys.length; i++) {
var key = keys[i];
if (key === '__ob__') { continue }
var provideKey = inject[key].from;
var source = vm;
/**
這個(gè)地方也有bug,source為當(dāng)前vue對(duì)象,
inject初始化發(fā)生在provide之前,
所以這里的source._provided第一次必為undefined
**/
while (source) {
if (source._provided && hasOwn(source._provided, provideKey)) {
result[key] = source._provided[provideKey];
break
}
source = source.$parent;
}
if (!source) {
if ('default' in inject[key]) {
var provideDefault = inject[key].default;
result[key] = typeof provideDefault === 'function'
? provideDefault.call(vm)
: provideDefault;
} else if (process.env.NODE_ENV !== 'production') {
warn(("Injection \"" + key + "\" not found"), vm);
}
}
}
return result
}
}
由此可以看出,inject繼承自最近父組件的provide,一旦找到就會(huì)break出尋找_provided的while循環(huán),如果沒(méi)有會(huì)一直找到根節(jié)點(diǎn)
順便提下個(gè)人主觀的issue: 尋找_provided的while循環(huán)中,進(jìn)入循環(huán)的source是不是一定沒(méi)有_provided?因?yàn)楫?dāng)前vm的provide初始化發(fā)生在inject初始化之后,所以這時(shí)候一定是undefined...吧?
provide初始化相關(guān)源碼:
function initProvide (vm) {
var provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide;
}
}
由此可以看出provide中的變量并沒(méi)有做過(guò)多處理,只是將_provide作為provide綁在了vm上,組件自身使用自己的provide屬性需要這樣寫(xiě): this._provide.xxx, _provide不是響應(yīng)式的,改變它的值不會(huì)引起view的變化
其使用方式為:
// 父組件:
provide: {
fa_provide: 一個(gè)常量
}
// 或
provide () {
return {
fa_provide: this.data中的變量
}
},
// 或
provide () {
return {
// fa_provide: this.obj.a
fa_provide: this.methods中的方法
}
},
// 子組件:可以引用/覆蓋/重寫(xiě)上層的provide
inject: ['fa_provide'],
provide: {
fa_provide: 另一個(gè)常量
}
// 孫子組件中也可以引用到父組件的provide
inject: ['fa_provide'],
然后通過(guò)this.fa_provide引用常量/變量,或者調(diào)用方法
個(gè)人對(duì)provide & inject 使用場(chǎng)景的理解是,跨級(jí)傳遞常量/變量/方法,供深層級(jí)子組件使用
4. $parent & $children
$parent & $children屬性的定義是發(fā)生在initMixin中。
initMixin僅僅只做了在Vue的原型上掛了個(gè)_init。
_init函數(shù)是在Vue構(gòu)建函數(shù)中唯一被調(diào)用的函數(shù)。
function Vue (options) {
this._init(options);
}
擴(kuò)展閱讀:
在_init函數(shù)中
Vue.prototype._init = function (options) {
...
/** 在這之前options中的結(jié)構(gòu)只包含
{
parent: VueComponent,
_isComponent: boolean,
_parentVnode: VNode
}
這里的options還是最原始的options
**/
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
}
...
initLifecycle(vm);
...
}
// initInternalComponent有這么幾行代碼
var opts = vm.$options = Object.create(vm.constructor.options);
opts.parent = options.parent;
opts._parentVnode = parentVnode;
這里會(huì)把你寫(xiě)的Vue文件中的data啊、methods啊,利用ES6的Object.create打到$option的__proto__上,其實(shí)你平時(shí)初始化Vue時(shí)調(diào)用的opts.data,opts.props之類(lèi)的屬性,并不是直接在opts上的,而是通過(guò)這里擴(kuò)展在原型鏈上的,parent也在擴(kuò)展范圍內(nèi)~
擴(kuò)展閱讀結(jié)束~回到正文
$parent & $children 的定義實(shí)際發(fā)生在initLifecycle中
function initLifecycle (vm) {
var parent = options.parent;
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
parent.$children.push(vm);
}
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
vm.$children = [];
}
使用方式也很簡(jiǎn)單,$children會(huì)獲取到一個(gè)包含所有子組件VueComponent對(duì)象的的數(shù)組,$parent會(huì)獲取到父節(jié)點(diǎn)對(duì)應(yīng)的Vue/VueComponent對(duì)象,你可以通過(guò)如下方式進(jìn)行操作
// 此處data_name代指data屬性值,function_name代指方法名
this.$children[index].children_data_name
this.$children[index].children_function_name
this.$parent.$parent.parent_data_name
this.$parent.$parent.parent_function_name
this.$root.root_data_name
this.$root.root_function_name
值得注意的是,我們通過(guò)腳手架構(gòu)建出來(lái)的Vue項(xiàng)目,$root是在main.js里寫(xiě)的那個(gè)new Vue({router,.......}).$mount('#app'),而不是我們寫(xiě)的那個(gè)App.vue
如果在層級(jí)很深的時(shí)候想拿到App.vue內(nèi)的data,可以this.$root.$children[0].app_data_name
5. $root
在上面第3節(jié)的結(jié)尾有一起提到~
PS: 后面的方法比較常用或者是語(yǔ)法糖,我準(zhǔn)備劃水通過(guò)了~
6. 自定義事件的 $emit & $on
$emit & $on是 Vue原型鏈上本來(lái)就綁定好的函數(shù),不是專(zhuān)門(mén)為了組件間通信而建立的,他們還能用來(lái)觸發(fā)一些鉤子函數(shù)。
父組件中如下引用子組件:
<emitCom @reverse='這里寫(xiě)父組件的方法名'></emitCom>
...
methods: {
reverse (val) {
this.father_name = val // 這里val為子組件觸發(fā)時(shí)傳遞的參數(shù)
}
}
子組件如下觸發(fā)
this.$emit('reverse','你被子元素觸發(fā)了')
7. sync語(yǔ)法糖
sync等于是幫你定義了一個(gè)自定義函數(shù),名為'update:' + 你v-bind的屬性名
父組件中如下引用子組件:
<syncCom :xxx.sync="father_name"></syncCom>
// 等效于
<syncCom :xxx="father_name" @update:xxx="val => {father_name = val}"></syncCom>
子組件如下觸發(fā)
this.$emit('update:xxx', '改變父組件!!!')
比較貼近生活的例子: elementUI中el-dialog中對(duì)顯隱變量visible的傳遞是使用的:visible.sync
8. vModel語(yǔ)法糖
萬(wàn)變不離其宗,這個(gè)vModel也是語(yǔ)法糖,效果就是平時(shí)寫(xiě)vModel雙向綁定+$emit的感覺(jué)差不多
父組件中如下引用子組件:
<child v-model="total"></child>
// 等效于
<child :xxx="total" @input='val => {total = val}'></child>
默認(rèn)狀態(tài)下:子組件如下觸發(fā)
this.$emit('input', xxx)
你也可以自定義傳過(guò)來(lái)的變量名和方法名
model: {
prop: 'parentValue', // 默認(rèn)值 value
event: 'change' // 默認(rèn)值 input
},
9. 粗暴的$refs獲取子組件
$refs一般被默認(rèn)為想要進(jìn)行一些Dom操作的時(shí)候才被使用,其實(shí)他也能夠獲得帶有ref屬性的子組件對(duì)象。
父組件中
<loading ref="loading"></loading>
<script>
showLoading () {
// 可以直接調(diào)用子組件中的方法,其實(shí)和$children相似
this.$refs.loading.showLoading()
setTimeout(() => {
this.$refs.loading.closeLoading()
},3000)
}
</script>
如果有大佬或者有興趣的小伙伴可以考究一下$refs的性能問(wèn)題,便利蜂的大佬說(shuō)$refs是操作了DOM,但是如果作用于Vue子節(jié)點(diǎn)的時(shí)候返回的明明是VueComponent對(duì)象,我感覺(jué)和$children沒(méi)太大區(qū)別,即時(shí)有區(qū)別也是因?yàn)?children是一定會(huì)初始化的,而$refs是在ast模板解析的時(shí)候根據(jù)你template中的ref來(lái)初始化的,如果你不寫(xiě)ref那性能必須比你寫(xiě)要好一丟丟~但是不管你寫(xiě)不寫(xiě)children,只要你有子組件就會(huì)有$children。可能就這些差異吧。
10. EventBus
- 引入單獨(dú)的空Vue文件
- 在需要接受響應(yīng)的頁(yè)面,引入該Vue文件,定義$on
import Bus from '@/api/bus.js'
...
Bus.$on('getTarget', target => {
...
});
3.在需要發(fā)起通知的頁(yè)面,引入該Vue文件,定義$emit
import Bus from '@/api/bus.js'
...
Bus.$emit('getTarget', 123);
11. Vuex
不適合作為小知識(shí)點(diǎn)擴(kuò)展,大致舉個(gè)例子,就是有些父子頁(yè)面、兄弟頁(yè)面或者更復(fù)雜關(guān)系的頁(yè)面,會(huì)使用Vuex來(lái)共享數(shù)據(jù),當(dāng)一個(gè)頁(yè)面改變了數(shù)據(jù),在另一個(gè)頁(yè)面我能通過(guò)compute(+watch),來(lái)做出相關(guān)的處理。嗯。。。我就當(dāng)你們都懂了~
12. 廢棄的$boradcast & $dispatch
這個(gè)我沒(méi)有自己使用過(guò),$dispatch 和 $broadcast在2.x版本已被廢棄,有興趣的小伙伴自行了解吧~