Vue組件的通信方式大致有這11(12)種
- 常用的Props
- $attrs & $listeners
- provide & inject
- $parent & $children
- $root
- 自定義事件的 $emit & $on
- sync語法糖(廢棄的修飾符 轉(zhuǎn) 語法糖)
- vModel語法糖
- 粗暴的$refs獲取子組件
- EventBus
- Vuex
- 廢棄的$boradcast & $dispatch
我只使用過前11種厅瞎,最后一個因為已經(jīng)廢棄驶社,也不作為語法糖,所以大家有興趣可以單獨去了解一下
1. props的使用
props是最基礎(chǔ)的組件單項數(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="這是個基本的彈層"></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往往會存在一個問題,因為props是單向數(shù)據(jù)流疚顷,也就是數(shù)據(jù)只能由父到子旱易,本身不提供子組件直接改變父組件的方式,只能父組件把自己的方法傳給子組件腿堤,再在子組件中回調(diào)父組件的方法阀坏,舉個簡單的例子,如果我寫一個名為tips的彈層提示組件笆檀,如果我把控制組件顯示邏輯的變量寫在了子組件里忌堂,父組件如何去改變子組件的變量值來顯示或隱藏子組件?如果不借助其他的方法似乎不能吧酗洒?所以只能把控制顯示的變量和相關(guān)方法都寫在父組件里士修,每個父組件都mixin相關(guān)的data和methods。感覺這樣寫比較死板樱衷,比如我要維護這個組件的時候棋嘲,需要改對應(yīng)組件的vue/js文件,還要去修改父組件的mixin.js箫老。
2. $attrs & $listeners
$attrs & $listeners 的初始化發(fā)生在生命周期 beforeCreate 之前的 initRender 函數(shù)中封字,使用 defineReactive(defineProperty) 將$attrs和$listener綁到了vm(vue對象)上,如果父組件傳遞的參數(shù)發(fā)生變動,會觸發(fā)updateChildComponent, 并對值進行更新
vm.$attrs = parentVnode.data.attrs || emptyObject;<br>
vm.$listeners = listeners || emptyObject;
$attrs表示父組件傳遞下來的props的集合
$listeners表示父組件傳遞下來的invoker函數(shù)的集合
舉個例子:
// 父組件中引用子組件
<attrAndListenersCom @setGrandData="setGrandData" :fatherdata='fa_data'></attrAndListenersCom>
在子組件中$attrs就是{fatherdata: 父組件中fa_data的值}
在子組件中$listeners就是 {setGrandData: ?}
然后子組件可以使用如下的方法阔籽,將父組件的參數(shù)繼續(xù)傳遞給自己的子組件
從而實現(xiàn)了父組件對孫子組件之間的數(shù)據(jù)傳遞
// 子組件中再引用其他子組件
<attrAndListenersComCom v-bind="$attrs" v-on="$listeners"></attrAndListenersComCom>
孫子組件簡易代碼如下
<template>
<div>孫子引用父組件的變量:{{$attrs.fatherdata}}</div>
<div class="btn" @click='test'>點我觸發(fā)一些操作</div>
</template>
<script>
methods: {
test () {
this.$emit('setGrandData', '孫子組件來了流妻!')
}
}
</script>
點擊按鈕,可以改變?nèi)齻€組件中笆制,對fa_data的引用绅这,即父組件的fa_data,子組件的$attrs.fatherdata在辆,和孫子組件中的$attrs.fatherdata
值得注意的是证薇,$attrs中不會出現(xiàn)被props引用過的值,也就是如果子組件的props引用了fatherdata匆篓,那他的$attrs就是空的浑度。這個過程發(fā)生在createComponent(組件創(chuàng)建)中,會調(diào)用extractPropsFromVNodeData函數(shù)鸦概,其內(nèi)部的checkProp函數(shù)會刪除$attrs中在props中出現(xiàn)的變量箩张。
還有就是:$attrs的賦值過程發(fā)生在updateChildComponent中,是一層一層往下傳遞的窗市,所以你在層級較高的組件中對$attrs進行watch先慷,watch的回調(diào)經(jīng)常會被觸發(fā)多次。但這并不是因為每一層都會響應(yīng)一次變動咨察,而是有點類似ReactHook中 useMemo 記憶組件的感覺:父組件有2個子組件a和b论熙,對a中參數(shù)的改變有可能會觸發(fā)b的重新渲染。個人理解這里也是一個道理摄狱,你的各種異步操作對父組件data的操作脓诡,觸發(fā)了updateChildComponent,最后都會響應(yīng)到深層子組件/$attrs的Watcher上媒役。
個人對 $attrs 使用場景的理解是:參數(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;
/**
這個地方也有bug刊愚,source為當前vue對象踊跟,
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商玫,一旦找到就會break出尋找_provided的while循環(huán),如果沒有會一直找到根節(jié)點
順便提下個人主觀的issue: 尋找_provided的while循環(huán)中牡借,進入循環(huán)的source是不是一定沒有_provided拳昌?因為當前vm的provide初始化發(fā)生在inject初始化之后,所以這時候一定是undefined...吧钠龙?
provide初始化相關(guān)源碼:
function initProvide (vm) {
var provide = vm.$options.provide;
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide;
}
}
由此可以看出provide中的變量并沒有做過多處理炬藤,只是將_provide作為provide綁在了vm上御铃,組件自身使用自己的provide屬性需要這樣寫: this._provide.xxx, _provide不是響應(yīng)式的沈矿,改變它的值不會引起view的變化
其使用方式為:
// 父組件:
provide: {
fa_provide: 一個常量
}
// 或
provide () {
return {
fa_provide: this.data中的變量
}
},
// 或
provide () {
return {
// fa_provide: this.obj.a
fa_provide: this.methods中的方法
}
},
// 子組件:可以引用/覆蓋/重寫上層的provide
inject: ['fa_provide'],
provide: {
fa_provide: 另一個常量
}
// 孫子組件中也可以引用到父組件的provide
inject: ['fa_provide'],
然后通過this.fa_provide引用常量/變量上真,或者調(diào)用方法
個人對provide & inject 使用場景的理解是,跨級傳遞常量/變量/方法羹膳,供深層級子組件使用
4. $parent & $children
$parent & $children屬性的定義是發(fā)生在initMixin中睡互。
initMixin僅僅只做了在Vue的原型上掛了個_init。
_init函數(shù)是在Vue構(gòu)建函數(shù)中唯一被調(diào)用的函數(shù)陵像。
function Vue (options) {
this._init(options);
}
擴展閱讀:
在_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;
這里會把你寫的Vue文件中的data啊就珠、methods啊,利用ES6的Object.create打到$option的__proto__上醒颖,其實你平時初始化Vue時調(diào)用的opts.data妻怎,opts.props之類的屬性,并不是直接在opts上的泞歉,而是通過這里擴展在原型鏈上的蹂季,parent也在擴展范圍內(nèi)~
擴展閱讀結(jié)束~回到正文
$parent & $children 的定義實際發(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 = [];
}
使用方式也很簡單,$children會獲取到一個包含所有子組件VueComponent對象的的數(shù)組疏日,$parent會獲取到父節(jié)點對應(yīng)的Vue/VueComponent對象,你可以通過如下方式進行操作
// 此處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
值得注意的是沟优,我們通過腳手架構(gòu)建出來的Vue項目,$root是在main.js里寫的那個new Vue({router,.......}).$mount('#app')睬辐,而不是我們寫的那個App.vue
如果在層級很深的時候想拿到App.vue內(nèi)的data挠阁,可以this.$root.$children[0].app_data_name
5. $root
在上面第3節(jié)的結(jié)尾有一起提到~
PS: 后面的方法比較常用或者是語法糖,我準備劃水通過了~
6. 自定義事件的 $emit & $on
$emit & $on是 Vue原型鏈上本來就綁定好的函數(shù)溯饵,不是專門為了組件間通信而建立的侵俗,他們還能用來觸發(fā)一些鉤子函數(shù)。
父組件中如下引用子組件:
<emitCom @reverse='這里寫父組件的方法名'></emitCom>
...
methods: {
reverse (val) {
this.father_name = val // 這里val為子組件觸發(fā)時傳遞的參數(shù)
}
}
子組件如下觸發(fā)
this.$emit('reverse','你被子元素觸發(fā)了')
7. sync語法糖
sync等于是幫你定義了一個自定義函數(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', '改變父組件0ァ!啄巧!')
比較貼近生活的例子: elementUI中el-dialog中對顯隱變量visible的傳遞是使用的:visible.sync
8. vModel語法糖
萬變不離其宗寻歧,這個vModel也是語法糖,效果就是平時寫vModel雙向綁定+$emit的感覺差不多
父組件中如下引用子組件:
<child v-model="total"></child>
// 等效于
<child :xxx="total" @input='val => {total = val}'></child>
默認狀態(tài)下:子組件如下觸發(fā)
this.$emit('input', xxx)
你也可以自定義傳過來的變量名和方法名
model: {
prop: 'parentValue', // 默認值 value
event: 'change' // 默認值 input
},
9. 粗暴的$refs獲取子組件
$refs一般被默認為想要進行一些Dom操作的時候才被使用秩仆,其實他也能夠獲得帶有ref屬性的子組件對象码泛。
父組件中
<loading ref="loading"></loading>
<script>
showLoading () {
// 可以直接調(diào)用子組件中的方法,其實和$children相似
this.$refs.loading.showLoading()
setTimeout(() => {
this.$refs.loading.closeLoading()
},3000)
}
</script>
如果有大佬或者有興趣的小伙伴可以考究一下$refs的性能問題澄耍,便利蜂的大佬說$refs是操作了DOM噪珊,但是如果作用于Vue子節(jié)點的時候返回的明明是VueComponent對象晌缘,我感覺和$children沒太大區(qū)別,即時有區(qū)別也是因為$children是一定會初始化的痢站,而$refs是在ast模板解析的時候根據(jù)你template中的ref來初始化的磷箕,如果你不寫ref那性能必須比你寫要好一丟丟~但是不管你寫不寫children,只要你有子組件就會有$children瑟押〔蠼荩可能就這些差異吧。
10. EventBus
- 引入單獨的空Vue文件
- 在需要接受響應(yīng)的頁面多望,引入該Vue文件嫩舟,定義$on
import Bus from '@/api/bus.js'
...
Bus.$on('getTarget', target => {
...
});
3.在需要發(fā)起通知的頁面,引入該Vue文件怀偷,定義$emit
import Bus from '@/api/bus.js'
...
Bus.$emit('getTarget', 123);
11. Vuex
不適合作為小知識點擴展家厌,大致舉個例子,就是有些父子頁面椎工、兄弟頁面或者更復(fù)雜關(guān)系的頁面饭于,會使用Vuex來共享數(shù)據(jù),當一個頁面改變了數(shù)據(jù)维蒙,在另一個頁面我能通過compute(+watch)掰吕,來做出相關(guān)的處理。嗯颅痊。殖熟。。我就當你們都懂了~
12. 廢棄的$boradcast & $dispatch
這個我沒有自己使用過斑响,$dispatch 和 $broadcast在2.x版本已被廢棄菱属,有興趣的小伙伴自行了解吧~