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

2019-04-22  本文已影响0人  zpkzpk

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

  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 @setGrandData="setGrandData" :fatherdata='fa_data'></attrAndListenersCom>

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

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

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

孙子组件简易代码如下

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

<script>
methods: {
    test () {
        this.$emit('setGrandData', '孙子组件来了!')
    }
}
</script>

点击按钮,可以改变三个组件中,对fa_data的引用,即父组件的fa_data,子组件的$attrs.fatherdata,和孙子组件中的$attrs.fatherdata

值得注意的是,$attrs中不会出现被props引用过的值,也就是如果子组件的props引用了fatherdata,那他的$attrs就是空的。这个过程发生在createComponent(组件创建)中,会调用extractPropsFromVNodeData函数,其内部的checkProp函数会删除$attrs中在props中出现的变量。

还有就是:$attrs的赋值过程发生在updateChildComponent中,是一层一层往下传递的,所以你在层级较高的组件中对$attrs进行watch,watch的回调经常会被触发多次。但这并不是因为每一层都会响应一次变动,而是有点类似ReactHook中 useMemo 记忆组件的感觉:父组件有2个子组件a和b,对a中参数的改变有可能会触发b的重新渲染。个人理解这里也是一个道理,你的各种异步操作对父组件data的操作,触发了updateChildComponent,最后都会响应到深层子组件/$attrs的Watcher上。

个人对 $attrs 使用场景的理解是:参数的逐层传递

3. provide & inject

inject的初始化发生在beforeCreate与created之间,先于provide的初始化

callHook(vm, 'beforeCreate');
initInjections(vm); // 初始化inject
initState(vm);
initProvide(vm); // 初始化provide
callHook(vm, 'created');

inject初始化相关源码:

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初始化发生在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循环,如果没有会一直找到根节点

顺便提下个人主观的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属性的子组件对象。

父组件中

<loading ref="loading"></loading>
<script>
    showLoading () {
        // 可以直接调用子组件中的方法,其实和$children相似
        this.$refs.loading.showLoading()
        setTimeout(() => {
            this.$refs.loading.closeLoading()
        },3000)
    }
</script>

如果有大佬或者有兴趣的小伙伴可以考究一下$refs的性能问题,便利蜂的大佬说$refs是操作了DOM,但是如果作用于Vue子节点的时候返回的明明是VueComponent对象,我感觉和$children没太大区别,即时有区别也是因为$children是一定会初始化的,而$refs是在ast模板解析的时候根据你template中的ref来初始化的,如果你不写ref那性能必须比你写要好一丢丢~但是不管你写不写children,只要你有子组件就会有$children。可能就这些差异吧。

10. EventBus

  1. 引入单独的空Vue文件
  2. 在需要接受响应的页面,引入该Vue文件,定义$on
import Bus from '@/api/bus.js'
...
Bus.$on('getTarget', target => {
    ...
});

3.在需要发起通知的页面,引入该Vue文件,定义$emit

import Bus from '@/api/bus.js'
...
Bus.$emit('getTarget', 123); 

11. Vuex

不适合作为小知识点扩展,大致举个例子,就是有些父子页面、兄弟页面或者更复杂关系的页面,会使用Vuex来共享数据,当一个页面改变了数据,在另一个页面我能通过compute(+watch),来做出相关的处理。嗯。。。我就当你们都懂了~

12. 废弃的$boradcast & $dispatch

这个我没有自己使用过,$dispatch 和 $broadcast在2.x版本已被废弃,有兴趣的小伙伴自行了解吧~

完~ ~ ~ 感谢收看,下期再见

上一篇下一篇

猜你喜欢

热点阅读