Vue組件間11種通信方式的簡(jiǎn)要介紹

Vue組件的通信方式大致有這11(12)種

  1. 常用的Props
  2. $attrs & $listeners
  3. provide & inject
  4. $parent & $children
  5. $root
  6. 自定義事件的 $emit & $on
  7. sync語(yǔ)法糖(廢棄的修飾符 轉(zhuǎn) 語(yǔ)法糖)
  8. vModel語(yǔ)法糖
  9. 粗暴的$refs獲取子組件
  10. EventBus
  11. Vuex
  12. 廢棄的$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

  1. 引入單獨(dú)的空Vue文件
  2. 在需要接受響應(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版本已被廢棄,有興趣的小伙伴自行了解吧~

完~ ~ ~ 感謝收看,下期再見(jiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,570評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,505評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,017評(píng)論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,786評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,219評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,438評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,971評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,796評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,995評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,230評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,662評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,918評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,697評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,991評(píng)論 2 374