Vue组件间的11种通信方式与简要介绍

Vue组件的通信方式大致有这11(12)种

其实这篇文章写了有一段时间了,最近写矩阵才意识到像平时那样只用vuex和props是远远不够的,掏出来分享一下
1. 常用的Props
2. $attrs & $listeners
3. provide & inject
4. $parent & $children
5. $root
6. 自定义事件的 $emit & $on
7. sync语法糖(废弃的修饰符 转 语法糖)
8. vModel语法糖
9. 粗暴的$refs获取子组件
10. EventBus
11. Vuex
12. 废弃的\$boradcast & \$dispatch

我只使用过前11种,最后一个因为已经废弃,也不作为语法糖,所以大家有兴趣可以单独去了解一下

1. props的使用

props是最基础的组件单项数据流通信,一般代码如下:

// 创建全局的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中的内容
export default {
    data () {
        return {
            show_tips: false
        }
    },
    methods: {
        showTips () {
            console.log(this)
            this.show_tips = true
            setTimeout(() => {
                this.show_tips = false
            },3000)
        }
    }
}

如果只使用props往往会存在一个问题,因为props是单向数据流,也就是数据只能由父到子,本身不提供子组件直接改变父组件的方式,只能父组件把自己的方法传给子组件,再在子组件中回调父组件的方法,举个简单的例子,如果我写一个名为tips的弹层提示组件,如果我把控制组件显示逻辑的变量写在了子组件里,父组件如何去改变子组件的变量值来显示或隐藏子组件?如果不借助其他的方法似乎不能吧?所以只能把控制显示的变量和相关方法都写在父组件里,每个父组件都mixin相关的data和methods。感觉这样写比较死板,比如我要维护这个组件的时候,需要改对应组件的vue/js文件,还要去修改父组件的mixin.js。

2. $attrs & $listeners

$attrs & $listeners 的初始化发生在生命周期 beforeCreate 之前的 initRender 函数中,使用 defineReactive(defineProperty) 将$attrs和$listener绑到了vm(vue对象)上,如果父组件传递的参数发生变动,会触发updateChildComponent, 并对值进行更新

    vm.$attrs = parentVnode.data.attrs || emptyObject;<br>
    vm.$listeners = listeners || emptyObject;

$attrs表示父组件传递下来的props的集合
$listeners表示父组件传递下来的invoker函数的集合

举个例子:

// 父组件中引用子组件
<attrAndListenersCom @getGrandData="getGrandData" :fatherdata='fa_data'></attrAndListenersCom>

在子组件中$attrs就是{fatherdata: 父组件中fa_data的值}
在子组件中$listeners就是 {getGrandData: ƒ}

然后子组件可以使用如下的方法,将父组件的参数继续传递给自己的子组件
从而实现了父组件对孙子组件之间的数据传递

// 子组件中再引用其他子组件
<attrAndListenersComCom v-bind="$attrs" v-on="$listeners"></attrAndListenersComCom>

孙子组件简易代码如下

<template>
    <div>孙子引用父组件的变量:{{$attrs.fatherdata}}</div>
    <div class="btn" @click='test'>点我触发一些操作</div>
</template>

}
}

由此可以看出,inject继承自最近父组件的provide,一旦找到就会break出寻找_provided的while循环,如果没有会一直找到根节点

顺便提下个人主观的issue: 寻找_provided的while循环中,进入循环的source是不是一定没有_provided?因为当前vm的provide初始化发生在inject初始化之后,所以这时候一定是undefined…吧?

provide初始化相关源码:

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不是响应式的,改变它的值不会引起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引用常量/变量,或者调用方法

个人对provide & inject 使用场景的理解是,跨级传递常量/变量/方法,供深层级子组件使用

4. $parent & $children

$parent & $children属性的定义是发生在initMixin中。
initMixin仅仅只做了在Vue的原型上挂了个_init。
_init函数是在Vue构建函数中唯一被调用的函数。

function Vue (options) {
    this._init(options);
}

扩展阅读:

在_init函数中


Vue.prototype._init = function (options) { ... /** 在这之前options中的结构只包含 { 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时调用的opts.data,opts.props之类的属性,并不是直接在opts上的,而是通过这里扩展在原型链上的,parent也在扩展范围内~

扩展阅读结束~回到正文

$parent & $children 的定义实际发生在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对象的的数组,$parent会获取到父节点对应的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

值得注意的是,我们通过脚手架构建出来的Vue项目,\$root是在main.js里写的那个new Vue({router,…….}).\$mount(‘#app’),而不是我们写的那个App.vue
如果在层级很深的时候想拿到App.vue内的data,可以this.\$root.\$children[0].app_data_name

5. $root

在上面第3节的结尾有一起提到~
PS: 后面的方法比较常用或者是语法糖,我准备划水通过了~

6. 自定义事件的 $emit & $on

$emit & $on是 Vue原型链上本来就绑定好的函数,不是专门为了组件间通信而建立的,他们还能用来触发一些钩子函数。

父组件中如下引用子组件:

<emitCom @reverse='这里写父组件的方法名'></emitCom>
...
    methods: {
        reverse (val) {
            this.father_name = val // 这里val为子组件触发时传递的参数
        }
    }

子组件如下触发

this.$emit('reverse','你被子元素触发了')

7. sync语法糖

sync等于是帮你定义了一个自定义函数,名为’update:’ + 你v-bind的属性名

父组件中如下引用子组件:

<syncCom :xxx.sync="father_name"></syncCom>

// 等效于

<syncCom :xxx="father_name" @update:xxx="val => {father_name = val}"></syncCom>

子组件如下触发

this.$emit('update:xxx', '改变父组件!!!')

比较贴近生活的例子: elementUI中el-dialog中对显隐变量visible的传递是使用的:visible.sync

8. vModel语法糖

万变不离其宗,这个vModel也是语法糖,效果就是平时写vModel双向绑定+$emit的感觉差不多
父组件中如下引用子组件:

<child v-model="total"></child>

// 等效于

<child :xxx="total" @input='val => {total = val}'></child>

默认状态下:子组件如下触发

this.$emit('input', xxx)

你也可以自定义传过来的变量名和方法名

model: {
    prop: 'parentValue', // 默认值 value
    event: 'change' // 默认值 input
},

9. 粗暴的$refs获取子组件

$refs一般被默认为想要进行一些Dom操作的时候才被使用,其实他也能够获得带有ref属性的子组件对象。

父组件中
“`

发表评论