Vue組件間11種通信方式的簡要介紹

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

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

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

完~ ~ ~ 感謝收看,下期再見

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末舰罚,一起剝皮案震驚了整個濱河市纽门,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌营罢,老刑警劉巖赏陵,帶你破解...
    沈念sama閱讀 218,682評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異饲漾,居然都是意外死亡瘟滨,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評論 3 395
  • 文/潘曉璐 我一進店門能颁,熙熙樓的掌柜王于貴愁眉苦臉地迎上來杂瘸,“玉大人,你說我怎么就攤上這事伙菊“苡瘢” “怎么了敌土?”我有些...
    開封第一講書人閱讀 165,083評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長运翼。 經(jīng)常有香客問我返干,道長,這世上最難降的妖魔是什么血淌? 我笑而不...
    開封第一講書人閱讀 58,763評論 1 295
  • 正文 為了忘掉前任矩欠,我火速辦了婚禮,結(jié)果婚禮上悠夯,老公的妹妹穿的比我還像新娘癌淮。我一直安慰自己,他們只是感情好沦补,可當我...
    茶點故事閱讀 67,785評論 6 392
  • 文/花漫 我一把揭開白布乳蓄。 她就那樣靜靜地躺著,像睡著了一般夕膀。 火紅的嫁衣襯著肌膚如雪虚倒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評論 1 305
  • 那天产舞,我揣著相機與錄音魂奥,去河邊找鬼。 笑死易猫,一個胖子當著我的面吹牛耻煤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播擦囊,決...
    沈念sama閱讀 40,358評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼嘴办!你這毒婦竟也來了瞬场?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評論 0 276
  • 序言:老撾萬榮一對情侶失蹤涧郊,失蹤者是張志新(化名)和其女友劉穎贯被,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妆艘,經(jīng)...
    沈念sama閱讀 45,722評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡彤灶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了批旺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片幌陕。...
    茶點故事閱讀 40,030評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汽煮,靈堂內(nèi)的尸體忽然破棺而出搏熄,到底是詐尸還是另有隱情棚唆,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評論 5 346
  • 正文 年R本政府宣布心例,位于F島的核電站宵凌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏止后。R本人自食惡果不足惜瞎惫,卻給世界環(huán)境...
    茶點故事閱讀 41,360評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望译株。 院中可真熱鬧瓜喇,春花似錦、人聲如沸古戴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽现恼。三九已至肃续,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間叉袍,已是汗流浹背始锚。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留喳逛,地道東北人瞧捌。 一個月前我還...
    沈念sama閱讀 48,237評論 3 371
  • 正文 我出身青樓,卻偏偏與公主長得像润文,于是被迫代替她去往敵國和親姐呐。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,976評論 2 355