Vue2总结(三)组件通信

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> Vue2总结(三)组件通信

趁着Vue3还没发布,先把vue2的各个知识点、源码、轮子全部温习一遍。 友情提示:阅读本文大概需要** 40****分钟**

前言

本篇文章将复习 Vue.js 父子组件之间通信的九种方式,无可否认,现在无论大厂还是小厂都已经用上了 Vue.js 框架,生态圈丰富,社区活跃,掌握 Vue 已经成为刚需而不是优势。Vue3发布之前,抓紧一起复习一下 Vue 的组件传值吧。

Vue2总结(三)组件通信

Vue组件传值

方式①.Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。


// Vuex 组件传值
// Vuex.js
const store = new Vuex.Store({
  strict:true,  // 开启严格模式  确保state 中的数据只能 mutations 修改
  state:{
    count:0
  },
  mutations:{ // 更改数据的方法
    add(state){
      state.count++
    },

    // 提交载荷用法
    // add(state,n){  
    //   state.count += n
    // },

    sub(state){
      state.count--
    }
  }
})

// 在组件的computed中使用
template
  div class="hello"
    button @click="add"+/button
    h2{{count}}/h2
    button @click="sub"-/button
  /div
/template

script
export default {
  name: 'HelloWorld',
  computed:{
     count(){
       return this.$store.state.count;
     }
  },
    methods:{
    add(){
      this.$store.commit('add');
    },

    //提交载荷用法
   // add(){  
   //    this.$store.commit('add',10);
   // },

   //对象风格的提交方式
   //   store.commit({
   //     type: 'add',
   //     n: 10
   //     })

    sub(){
      this.$store.commit('sub');
    }
  }
}
/script

优点:

  • 解决了多层组件之间繁琐的事件传播。
  • 解决了多组件依赖统同一状态的问题。
  • 单向数据流
  • 为Vue量身定做,学习成本不高
  • 缺点:

  • 不能做数据持久化,刷新页面就要重制,要做数据持久化可以考虑使用localstorage,或者相关插件(比如 vuex-persistedstate)。
  • 增加额外的代码体积,简单的业务场景不建议使用。
  • 方式②.EventBus

    通过共享一个vue实例,使用该实例的 $on以及 $emit实现数据传递。

    
    // EventBus 使用方法
    // bus.js
    import Vue from 'vue'
    export default new Vue({})
    
    // 组件a
    import bus from './bus.js'
    export default {
      created () {
        bus.$on('event-name', (preload) = {
          // ...
        })
      }
    }
    
    // 组件b
    import bus from './bus.js'
    export default {
      created () {
        bus.$emit('event-name', preload)
      }
    }
    

    优点:

  • 解决了多层组件之间繁琐的事件传播。
  • 使用原理十分简单,代码量少。
  • 缺点:

  • 由于是都使用一个Vue实例,所以容易出现重复触发的情景,例如: 1.多人开发时,A、B两个人定义了同一个事件名。 2.两个页面都定义了同一个事件名,并且没有用`$off`销毁(常出现在路3.由切换时)。在for出来的组件里注册。

  • 项目一大用这种方式管理事件会十分混乱,这时候建议用vuex。
  • 方式③.props和 $emit/$on

    最基本的父组件给子组件传递数据方式,将我们自定义的属性传给子组件,子组件通过$emit方法,触发父组件v-on的事件,从而实现子组件触发父组件方法。

    $attrs/ $listeners可以将父组件的props和事件监听器继承给子元素,在子组件可以调用到父组件的事件和props。

    
    // props组件传值最常用
    // asyncData为异步获取的数据,想传递给子组件使用
    // 父组件
    template
      div
        父组件
        child :child-data="asyncData"/child
      /div
    /template
    
    script
    import child from './child'
    export default {
       data: () = ({
         asyncData: ''
       }),
       components: {
         child
       },
       created () {
       },
       mounted () {
         // setTimeout模拟异步数据
         setTimeout(() = {
           this.asyncData = 'async data'
         console.log('parent finish')
         }, 2000)
       }
    }
    /script
    
    // 子组件
    template
     div
      子组件{{childData}}
     /div
    /template
    
    script
    export default {
       props: ['childData'],
         data: () = ({
       }),
       created () {
         console.log(this.childData) // 空值
       },
       methods: {
       }
    }
    /script
    // prop 也经常和 $emit/$on同时使用
    
    // 仅仅使用$emit/$on传值,可以首先新建一个bus.js
    // 父组件与子组件都要import bus.js
    import {bus} from '../../bus.js'
    
    created(){
      bus.$on('custTreeSay',(id)={
      //监听传值--机构代码
      this.instCode = id;
      //关闭弹窗
      this.popupVisibleTree = false;
      //调用查询方法刷新通讯录列表
      this.query();
      });
      bus.$on('custTreeSayName',(name)={
      //监听传值--机构名称
      this.instName = name;
      });
    }
    
    // 在子组件中定义点击事件,调用父组件方法通过$emit将相应值传给父组件
    span @click="propInstCode(model);propInstName(model)"
      {{model.name}}
    /span
    
    script type="text/javascript"
    import {bus} from '../../bus.js'
    export default {
       props: ['model'],//这里通过props接收父组件的传值
    
       //method钩子定义传值方法,这边需要传不同的值
       methods: {
         //通过总线将值传给父组件
         propInstCode:function (model) {
         //$emit触发当前实例事件
           bus.$emit('custTreeSay',this.model.id);
         },
         propInstName:function (model) {
           bus.$emit('custTreeSayName',this.model.name);
         }
      },
    }
    

    优点:

  • 使用最为简单,也是父子组件传递最常见的方法。
  • Vue为给props提供了类型检查支持。
  • `$emit`不会修改到别的组件的同名事件,因为他只能触发父级的事件,这里和event-bus不同
  • 缺点:

  • 单一组件层级一深需要逐层传递,会有很多不必要的代码量。
  • 不能解决了多组件依赖统同一状态的问题。
  • 方式④.provide / inject

    简单的来说就是在父组件中通过provider来提供变量,然后在子组件中通过inject来注入变量。需要注意的是这里不论子组件有多深,只要调用了inject那么就可以注入provider中的数据。而不是局限于只能从当前父组件的prop属性来获取数据。

    
    // provide / inject 组件传值
    // 父组件
    template
        div
          comp-one ref = "comp"
            span slot-scope = "props" ref="span"{{props.value}} {{props.aaa}} {{value}}/span
          /comp-one
        /div
    /template
    
    new Vue({
      components: {
        CompOne: ChildComponent
      },
      //父组件通过provide将自己的数据以对象形式传出去
      provide () {
        return {
          yeye: this
          value: this.value
        }
      },
      el: '#root',
      data () {
        return {
          value: '本组件的123'
        }
      },
      mounted () {
        console.log(this.$refs.comp.value)
        console.log(this.$refs.span)
      }
    
    
    // 子组件1
    div :style = "style"
       slot :value = "value" :aaa = "aaa"/slot
       sun-child-component/sun-child-component
    /div
    
    const ChildComponent = {
      name: 'comp',
      components: {
        SunChildComponent
      },
      data () {
        return {
          value: 'component val',
          aaa: 'component aaa'
        }
      }
    }
    
    
    // 子组件2
    divchild component/div
    
    const SunChildComponent = {
      // 跨级使用了父组件的数据
      inject: ['yeye'],              
      mounted () {
        console.log(this.yeye)
      }
    }
    

    优点:

  • 不用像props一层层传递,由于vue有`$parent`属性可以让子组件访问父组件。如果孙组件想要访问祖先组件,就可以通过 provide/inject可以轻松实现跨级访问父组件的数据,可以跨层级传递。
  • 缺点:

  • 用这种方式传递的属性是非响应式的,所以尽可能来传递一些静态属性。
  • 引用官网的话是它将你的应用以目前的组件组织方式耦合了起来,使重构变得更加困难。,我对这句话的理解是用了 provide/inject 你就要遵循它的组件组织方式,在项目的重构时如果要破坏这个组织方式会有额外的开发成本,其实event-bus也有这个问题。
  • 方式⑤.slot

    2.1.0新增作用域插槽,你可以在组件的html模版里添加自定义内容,这个内容可以是任何代码模版,就像:

    
    navigation-link url="/profile"
      !-- 添加一个 Font Awesome 图标 --
      span class="fa fa-user"/span
      Your Profile
    /navigation-link
    

    父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。你也可以通过slot-scope属性来实现从子组件将一些信息传递给父组件,注意这个属性是vue2.1.0+新增的。

    
    // slot 组件传值
    // App.vue
    template
        l-input value='用户名' /
    /template
    
    // 父组件
    templale
        div class="control"
            slot :is-group="isGroup"/slot
        /div
    /template
    script
        export default {
            data () {
                isGroup: true
            }
        }
    /script
    
    // 子组件
    template
        template v-if="!isGroup" 
            div class="control"
               input type='text' :value='value' /
            /div
        /template
        template
            input type='text' :value='value' /
        /template
    /template
    
    script
    export default {
        props: {
           value: {
                type: ''
           }
        },
        computed: {
        }
    }
    /script
    

    优点:

  • 可以在父组件里自定义插入到子组件里的内容,虽然其他属性也可以,但是我觉得 slot 更倾向于自定义的条件是来自于父容器中。
  • 复用性好,适合做组件开发。 缺点:

  • 和props一样不支持跨层级传递。
  • 方式⑥.$parent / $children

    通过 $parent/ $children可以拿到父子组件的实例,从而调用实例里的方法,实现父子组件通信。并不推荐这种做法。可以使用  this.$parent查找当前组件的父组件,使用 this.$children查找当前组件的直接子组件,可以遍历全部子组件, 需要注意 $children 并不保证顺序,也不是响应式的,也可以使用 this.$root查找根组件,并可以配合 $children遍历全部组件,也可以使用 this.$refs查找命名子组件。
    使用方面通过 this.$parent或者 this.$children拿到父或子组件实例。

    
    // $parent / $children 组件传值
    // 父组件 Game.vue
    template
    div class="game"
        h2{{ msg }}/h2
        LOL ref="lol"/LOL
        DNF ref="dnf"/DNF
    /div
    /template
    script
    import LOL from '@/components/game/LOL'
    import DNF from '@/components/game/DNF'
    export default {
        name: 'game',
        components: {
            LOL,
            DNF
        },
        data () {
            return {
                msg: 'Game',
                lolMsg:'Game-LOL',
                dnfMsg:'Game-DNF',
            }
        },
        methods: {
    
        },
        mounted(){ //注意 mounted
    
            //读取子组件数据,注意$children子组件的排序是不安全的
            console.log(this.$children[0].gameMsg);//LOL-Game
    
            //读取命名子组件数据
            console.log(this.$refs.dnf.gameMsg);//DNF-Game
    
            //从根组件查找组件数据
            console.log(this.$root.$children[0].msg); //APP
            console.log(this.$root.$children[0].$children[0].msg); //Game
            console.log(this.$root.$children[0].$children[0].$children[0].msg); //Game-LOL
            console.log(this.$root.$children[0].$children[0].$children[1].msg); //Game-DNF
        }
    }
    /script
    style lang="css"
    .game{
        border: 1px solid #00FF00;
        width: 200px;
    }  
    /style
    
    // 子组件 LOL.vue
    template
      div class="lol"
        h2{{ msg }}/h2
      /div
    /template
    
    script
    export default {
        name: 'LOL',
        data () {
            return {
                msg: 'LOL',
                gameMsg:'LOL-Game',
            }
        },
        methods:{
    
        },
        created(){
            //读取父组件数据
            this.msg = this.$parent.lolMsg;
        }
    }
    /script
    
    
    // DNF.vue
    template
      div class="dnf"
        h2{{ msg }}/h2
      /div
    /template
    
    script
    import Bus from '../../utils/bus.js'
    export default {
        name: 'DNF',
        data () {
            return {
                msg: 'DNF',
                gameMsg:'DNF-Game',
            }
        },
        methods:{
    
        },
        created(){
            //从根组件向下查找父组件数据
            this.msg = this.$root.$children[0].$children[0].dnfMsg;
        }
    }
    /script
    

    优点:

  • 可以拿到父子组件实例,从而拥有实例里的所有属性。
  • 缺点:

  • 用这种方法写出来的组件十分难维护,因为你并不知道数据的来源是哪里,有悖于单向数据流的原则
  • `this.$children`拿到的是一个数组,你并不能很准确的找到你要找的子组件的位置,尤其是子组件多的时候。

  • 方式⑦. .sync 修饰符

    它在 vue@1.x 的时候曾作为双向绑定功能存在,即子组件可以修改父组件中的值。因为它违反了单向数据流的设计理念,所以在 vue@2.0 的时候被干掉了。但是在 vue@2.3.0+ 以上版本又重新引入了这个 .sync 修饰符。但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 v-on 监听器。说白了就是让我们手动进行更新父组件中的值了,从而使数据改动来源更加的明显。下面引入自官方的一段话:

    在有些情况下,我们可能需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。

    既然作为一个语法糖,肯定是某种写法的简写形式,直接看demo:

    
    text-document
      v-bind:title="doc.title"
      v-on:update:title="doc.title = $event"
    /text-document
    

    于是我们可以用 .sync 语法糖简写成如下形式:

    
    text-document v-bind:title.sync="doc.title"/text-document
    

    假如我们想实现这样一个效果:改变子组件文本框中的值同时改变父组件中的值。怎么做?列位不妨先想想。先看段代码:

    
    // 
    div class="input-group"
       label姓名:/label
       input v-model="text"
    /div
    
    let Login = Vue.extend({
      props: ['name'],
      data () {
        return {
          text: ''
        }
      },
      watch: {
        text (newVal) {
          this.$emit('update:name', newVal)
        }
      }
    })
    
    new Vue({
      el: '#app',
      data: {
        userName: ''
      },
      components: {
        Login
      }
    })
    

    注意代码里有这一句话:

    
    this.$emit('update:name', newVal)
    

    官方语法是:update:myPropName 其中 myPropName 表示要更新的 prop 值。当然如果你不用 .sync 语法糖使用上面的  .$emit 也能达到同样的效果。仅此而已。

    方式⑧.$attrs 和 $listeners

    官网对 $attrs 的解释如下:包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过  v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

    官网对 $listeners 的解释如下:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过  v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

    我觉得 $attrs $listeners 属性像两个收纳箱,一个负责收纳属性,一个负责收纳事件,都是以对象的形式来保存数据。看下面的代码解释:

    
    div id="app"
      child 
        :foo="foo" 
        :bar="bar"
        @one.native="triggerOne"
        @two="triggerTwo"
      /child
    /div
    

    从 Html 中可以看到,这里有俩属性和俩方法,区别是属性一个是 prop 声明,事件一个是 .native 修饰器。

    
    let Child = Vue.extend({
      template: 'h2{{ foo }}/h2',
      props: ['foo'],
      created () {
        console.log(this.$attrs, this.$listeners)
        // - {bar: "parent bar"}
        // - {two: fn}
    
        // 这里我们访问父组件中的 `triggerTwo` 方法
        this.$listeners.two()
        // - 'two'
      }
    })
    
    new Vue({
      el: '#app',
      data: {
        foo: 'parent foo',
        bar: 'parent bar'
      },
      components: {
        Child
      },
      methods: {
        triggerOne () {
          alert('one')
        },
        triggerTwo () {
          alert('two')
        }
      }
    })
    

    我们可以通过 $attrs $listeners 进行数据传递,在需要的地方进行调用和处理,还是很方便的。当然,我们还可以通过  v-on="$listeners" 一级级的往下传递,

    方式⑨.broadcast / dispatch

    broadcast 也叫事件广播,dispatch 是查找所有父级,直到找到要找到的父组件,并在身上触发指定的事件。它们俩是 vue@1.0 中的方法,在 vue@2.0 里面已经删掉了。最近在使用 Element 过程中发现组件通信大量使用 dispatch 和 broadcast 两个方法,之前在 vue2 组件通信 也提到过 vue2 中取消了  $dispatch $broadcast 两个重要的事件,而 Element 重新实现了这两个函数。

    
    // broadcast / dispatch 组件传值
    // element-ui/lib/mixins/emitter 里的 emitter.js
    "use strict";
    exports.__esModule = true;
    function _broadcast(componentName, eventName, params) {
      this.$children.forEach(function (child) {
        var name = child.$options.componentName;
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params));
        } else {
          _broadcast.apply(child, [componentName, eventName].concat([params]));
        }
      });
    }
    exports.default = {
      methods: {
        dispatch: function dispatch(componentName, eventName, params) {
          var parent = this.$parent || this.$root;
          var name = parent.$options.componentName;
          while (parent && (!name || name !== componentName)) {
            parent = parent.$parent;
            if (parent) {
              name = parent.$options.componentName;
            }
          }
          if (parent) {
            parent.$emit.apply(parent, [eventName].concat(params));
          }
        },
        broadcast: function broadcast(componentName, eventName, params) {
          _broadcast.call(this, componentName, eventName, params);
        }
      }
    };
    

    解析:

  • dispatch 和 broadcast 方法都需要 3 个参数,componentName 组件名称, eventName 事件名称, params 传递的参数。
  • dispatch 方法会寻找所有的父组件,直到找到名称为 componentName 的组件,调用其 `$emit()` 事件。broadcast 方法则是遍历当前组件的所有子组件,找到名称为 componentName 的子组件,然后调用其 `$emit()` 事件。
  • 这里也看出了 Element 中的 dispatch 与 broadcast 的不同,vue1 中的 `$dispatch` 和 `$broadcast` 会将事件通知给所有的 父/子 组件,只要其监听了相关事件,都能够(能够,不是一定)触发;而 Element 则更像是定向爆破,指哪打哪,其实更符合我们日常的需求。
  • dispatch 方法会寻找所有的父组件,直到找到名称为 componentName 的组件,调用其 $emit() 事件。broadcast 方法则是遍历当前组件的所有子组件,找到名称为 componentName 的子组件,然后调用其  $emit() 事件。

    使用方式

  • 兄弟组件之间的通信可以很好的诠释上述两个事件。假设父组件 App.vue 中引入了两个子组件 Hello.vue 和 Fuck.vue。
  • 如果你的项目中巧合使用了 Element,那可以按照下面的方式将其引入进来,如果没有用 Element 也不用担心,复制上面的 emitter.js,通过 mixins 的方式引入即可。
  • 
    // 在 App.vue 中监听 message 事件,收到事件后,
    // 通过 broadcast 和接收到的参数,将事件定向传播给相关组件。
    template
      div id="app"
        hello/hello
        fuck/fuck
      /div
    /template
    script
      import Hello from 'components/Hello'
      import Fuck from 'components/Fuck'
      import Emitter from 'element-ui/lib/mixins/emitter'
      export default {
        name: 'app',
        componentName: 'ROOT',
        mixins: [Emitter],
        components: {
          Hello,
          Fuck
        },
        created () {
          this.$on('message', params = {
            this.broadcast(params.componentName, params.eventName, params.text)
          })
        }
      }
    /script
    
    // Fuck.vue
    import Emitter from 'element-ui/lib/mixins/emitter'
    import event from 'mixins/event'
    export default {
      componentName: 'Fuck',
      mixins: [Emitter, event],
      data () {
        return {
          name: 'Fuck',
          textarea: '',
          tableData: []
        }
      },
      methods: {
        submit () {
          this.communicate('message', {
            componentName: 'Hello',
            text: this.textarea
          })
          this.textarea = ''
        }
      },
      created () {
        this.$on('message', text = {
          this.tableData.push(this.getMessage(text))
        })
      }
    }
    
    // mixins/event.js
    import Emitter from 'element-ui/lib/mixins/emitter'
    export default {
      mixins: [Emitter],
      methods: {
        communicate (event, params = {}) {
          this.dispatch('ROOT', event, Object.assign({
            eventName: event
          }, params))
        }
      }
    }
    

    其中 Fuck.vue 中监听了 message 事件,当收到消息时,向 tableData 中加入新的值。而 summit 方法则调用 event.js 中的 communicate 方法,通过 dispatch 方法将事件传播给 ROOT 组件。

    总结

    vue3 之前的组件通信方式总结:

  • 父组件向子组件传递信息使用 props down
  • 子组件向父组件传递信息使用 event up
  • 其它关系类型组件通信使用 global event bus
  • 大型 SPA 组件之间通信使用 Vuex 管理组件状态
  • 使用 Element 下 emitter.js 中的 dispatch 和 broadcast 做事件定向传播
  • 参考

    1.小鱼-Vue.js 父子组件之间通信的十种方式 - 链接 http://www.yyyweb.com/5189.html

    最后

    今天的 Vue2总结(三)组件通信 就分享到这里,我的公众号没有留言功能哈,有问题大家心里默念,我能感受到,谢谢 ~ 

    原文始发于微信公众号(程序员思语):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> Vue2总结(三)组件通信


     上一篇
    Vue2总结(五)单元测试 Vue2总结(五)单元测试
    趁着Vue3还没发布,先把vue2的各个知识点、源码、轮子全部温习一遍,今天的主题是Vue项目的单元测试重要性及其使用。 友情提示:阅读本文大概需要** 30****分钟** 前言当你的项目足够大的时候,在叠加模块和组件的过程中,是
    2021-04-05
    下一篇 
    Vue2总结(二)Vuex Vue2总结(二)Vuex
    趁着Vue3还没发布,先把vue2的各个知识点、源码、轮子全部温习一遍。 友情提示:阅读本文大概需要** 30****分钟** 前言常见的前端状态管理库有Redux、mobX、Vuex、以及上次介绍的Fish-redux。其中Vue
    2021-04-05