美文网首页
Vue组件间11种通信方式的简要介绍

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

作者: zpkzpk | 来源:发表于2019-04-22 15:05 被阅读0次

    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版本已被废弃,有兴趣的小伙伴自行了解吧~

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

    相关文章

      网友评论

          本文标题:Vue组件间11种通信方式的简要介绍

          本文链接:https://www.haomeiwen.com/subject/jlaegqtx.html