Vue组件通信大全(终结篇)

背景

  Vue是单页面应用,单页面应用又是由组件构成,各个组件之间又互相关联,那么如何实现组件之间通信就显得尤为重要了。就像人是由各种器官组成,那么组件之间的通信就像是血液一样将营养(数据)输送到各个部位,为了保证数据流向的简洁性,使程序更易于理解,所以vue提倡单项数据流。组件之间的通信主要分为三种,父子组件通信、孙子组件通信和非关联组件通信。

父子组件通信

props和$emit

  这种方式是大家最经常用到的,父组件通过v-bind绑定数据,子组件通过props接收父组件传过来的数据,利用$emit触发指定事件,父组件通过$on监听子组件触发的对应事件,这里就不举例了。主要讲一下prop:

  • Vue数据是单向数据流,这样设计的目的是为了保证数据流向的简洁性,使程序更易于理解,每次父级组件发生更新时,子组件中所有prop都将会刷新为最新的值,prop会在子组件创建之前传递,所以可以在data和computed中直接使用。
  • 不应该在一个子组件内部改变prop,这样会破坏单向的数据绑定,导致数据流难以理解。如果有这样的需要,可以通过 data 属性接收或使用 computed 属性进行转换。
  • 如果prop传递的是基本类型,这时候改变prop的数据就会报错,如果是引用数据类型如果改变原始数据不会报错,但是重新赋值或改变某个属性值就会报错,利用这一点就能够实现父子组件数据的“双向绑定”,虽然这样实现能够节省代码,但会牺牲数据流向的简洁性,令人难以理解,最好不要这样去做。想要实现父子组件的数据“双向绑定”,可以使用 v-model 或 .sync,下面会讲到。

v-model

v-model实现父子组件数据的双向绑定,它的本质是v-bind和v-on的语法糖,在一个组件上使用v-model,默认会为组件绑定名为value的属性和名为input的事件。例如:

<test-model
v-bind:value="haorooms"
v-on:input="haorooms=$event"></test-model>
<script>
export default {
   data () {
     haorooms: ''
   }
}
</script>

等价于

<test-model v-model="haorooms"></test-model>
<script>
export default {
   data () {
     haorooms: ''
   }
}
</script>

子组件

<template>
<div>
  <input  
   v-bind:value="value"
   v-on="$emit('input', $event.target.value)">
</div>
</template>
<script>
  export default {
    props: ['value'],
    model: {
       prop: 'value',
       event: 'input'
    }
  }
</script>

.sync

.sync修饰符它的本质和v-model类似,它的本质也是v-bind和v-on的语法糖,例如:

<test-model
v-bind:title="doc.title"
v-on:update:title="doc.title=$event"></test-model>
<script>
export default {
   data () {
     doc: {
        title: ''
     }
   }
}
</script>

等价于

<test-model v-bind:title.sync="doc.title"></test-model>
<script>
export default {
   data () {
      doc: {
        title: ''
     }
   }
}
</script>

子组件

<template>
<div>
  <input v-model="value">
</div>
</template>
<script>
  export default {
     data () {
        value: ''
     },
    watch: {
       value (val) {
          this.$emit('update:title', val)
       }
    }
  }
</script>

这种是绑定一个值的情况,还可以绑定多个值:

<template>
 <div id="demo">
  <test-model v-bind.sync="haorooms"></test-model>
</div>
</template>
<script>
  import testModel from './testModel'
  export default {
   data(){
     return{
       haorooms: {
         name: 'aaa',
         age: 18,
         value: 10
       }
     }
  },
  components: {
     testModel,
  },
  watch: {
     haorooms: {
       handler (val) {
         console.log('test', val)
       },
       deep: true
      }
  }
}
</script>

子组件

<template>
    <div></div>
</template>
<script>
    export default {
        data () {
            return {
                test: ''
            }
        },
        mounted () {
            this.$emit('update:name', 111)
        }
    }
</script>

我们看到这种方式是将haorooms对象中name、age、value三个属性都实现了双向绑定,在子组件中触发事件的时候需要指定某个属性来触发。

注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用 (例如 v-bind:title.sync=”doc.title + ‘!’” 是无效的)。取而代之的是,你只能提供你想要绑定的属性名,类似 v-model。
将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的,因为在解析一个像这样的复杂表达式的时候,有很多边缘情况需要考虑。

v-model和.sync对比

共同点:

  • v-model和.sync都可以实现父子组件数据的双向绑定,本质都是v-bind和v-on的语法糖。
    不同点:
  • v-model默认会为组件绑定名为value的属性和名为input的事件,而.sync可以自定义传入的属性和事件。
  • v-model只能实现某一个属性的双向绑定,.sync可以实现多个属性的双向绑定。
  • 写法不同v-model需要在子组件中写porp来接收数据,而.sync是利用$attrs来接收数据,所以不需要写prop。

$parent、$children和ref

  这三种方式都是通过直接得到组件实例,可以实现父子组件、兄弟组件、跨级组件等数据通信,不过一般不建议使用,因为会增加组件间的耦合,而且要判断组件存不存在,如果不存在可能会遇到报错,这几种方式比较简单也不在这里举例了。

跨级组件

$attrs和$listeners

$attrs
背景

  随着项目复杂度的提高,组件嵌套的层级越来越深,之前的组件通信一般使用v-bind和prop组合使用,但是我们发现这种方式只是适用于父子组件,如果是孙子组件的话就需要将父组件的数据传递给子组件,子组件的数据再传递给孙组件,这样子就需要写很多prop,有没有哪种方式可以直接将父组件直接传递给孙组件,让代码更加简洁?这就是$attrs的由来,解决跨组件数据传递,注意只对孙子组件有效,但是class和style数据传递除外。

使用

首先我们有三个嵌套组件父A-子B-孙C,然后我们想让A中的数据传入C中,用prop的做法是这样子的:

  <div id="app">
    A{{msg}}
    <component-b :msg="msg"></component-b>
  </div>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: '100'
      },
      components: {
        'ComponentB': {
          props: ['msg'],
          template: `<div>B<component-c :msg="msg"></component-c></div>`,
          components: {
            'ComponentC': {
              props: ['msg'],
              template: '<div>C{{msg}}</div>'
            }
          }
        },
        
      }
    })
  </script>

组件B并没有使用父组件传递过来的msg,而是直接传递给组件C,除了这样子有没有什么方式直接将数据传递给C呢,下面我们来看$attrs的写法:

  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: '100'
      },
      components: {
        'ComponentB': {
          template: `<div>B<component-c v-bind="$attrs"></component-c></div>`,
          components: {
            'ComponentC': {
              props: ['msg'],
              template: '<div>C{{msg}}</div>'
            }
          }
        },
        
      }
    })
  </script>

总结:为了解决跨组件通信,而提出了$attrs。prop和$attrs都可以用来父子组件通信,接收父组件传递过来的数据,但是prop的优先级高于$attrs,如果子组件中prop、$attrs都有写,那么数据只会被prop接收,注意$attrs不能接收class和style传过来的数据。

inheritAttrs

背景
<template>
  <div class="home">
    <mytest  :title="title" :message="message"></mytest>
  </div>
</template>
<script>
export default {
  name: 'home',
  data () {
    return {
      title:'title1111',
      message:'message111'
    }
  },
  components:{
    'mytest':{
      template:`<div>这是个h1标题{{title}}</div>`,
      props:['title'],
      data(){
        return{
          meg:'111'
        }
      },
      created:function(){
        console.log(this.$attrs)//注意这里
      }
    }
  }
}
</script>

  上面的代码,我们在组件里只是用了title这个属性,message属性没有用到,那么下浏览器渲染出来是什么样呢?如下图:


image.png

我们看到:组件内未被注册的属性将作为普通html元素属性在子组件的根元素上渲染,虽然在一般情况下不会对子组件造成影响,但是就怕遇到一些特殊情况,比如:

<template>
    <childcom :name="name" :age="age" type="text"></childcom>
</template>
<script>
export default {
    'name':'test',
    props:[],
    data(){
        return {
            'name':'张三',
            'age':'30',
            'sex':'男'
        }
    },
    components:{
        'childcom':{
            props:['name','age'],
            template:`<input type="number" style="border:1px solid blue">`,
        }
    }
}
</script>
image.png

我们看到父组件的type="text"覆盖了input上type="number",这不是我想要的,我需要input上type=number类型不变,但是我还是想取到父组件上的type="text"的值,这时候inheritAttrs就派上用场了。

<template>
    <childcom :name="name" :age="age" type="text"></childcom>
</template>
<script>
export default {
    'name':'test',
    props:[],
    data(){
        return {
            'name':'张三',
            'age':'30',
            'sex':'男'
        }
    },
    components:{
        'childcom':{
            inheritAttrs:false,
            props:['name','age'],
            template:`<input type="number" style="border:1px solid blue">`,
            created () {
                console.log(this.$attrs.type)
            }
        }
    }
}
</script>
image.png

总结:默认情况下父组件传递数据给子组件但是没被prop特性绑定将会回退且作为普通的html特性应用在子组件的根元素上。inheritAttrs属性用来去掉这种默认行为,来避免不可预知的影响。注意 inheritAttrs: false 选项不会影响 style 和 class 的绑定。

$listeners

背景

上面讲了$attrs是为了跨组件传递数据,那如果想通过孙子组件来给父组件传递数据呢?之前的做法也是一层一层的向上传递,比如用$emit方法,但是子组件如果用不到,只是想改变父组件的数据,这时候我们就可以使用$listeners。

<template>
    <div>
        <childcom :name="name" :age="age" :sex="sex" @testChangeName="changeName"></childcom>
    </div>
</template>
<script>
export default {
    'name':'test',
    props:[],
    data(){
        return {
            'name':'张三',
            'age':'30',
            'sex':'男'
        }
    },
    components:{
        'childcom':{
            props:['name'],
            template:`<div>
                <div>我是子组件   {{name}}</div>
                <grandcom v-bind="$attrs" v-on="$listeners"></grandcom>
            </div>`,
           
            components: {
                'grandcom':{
                    template:`<div>我是孙子组件-------<button @click="grandChangeName">改变名字</button></div>`,
                    methods:{
                        grandChangeName(){
                           this.$emit('testChangeName','kkkkkk')
                        }
                    }
                }
            }
        }
    },
    methods:{
        changeName(val){
            this.name = val
        }
    }
}
</script>

$listeners是一个对象,里面包含了作用在这个组件上的所有监听器。

参考文章:https://www.jianshu.com/p/ce8ca875c337

provide和inject

背景

  项目越复杂,组件嵌套的层级就越深,那么子组件怎样实现跟祖先组件的通信问题,这就是provide和inject提出的原因。

简介

  这个方法允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件嵌套的层次有多深,并在起上下游关系成立的时间里始终有效。一言以蔽之:祖先组件中通过provider来提供变量,然后在子孙组件中通过inject来注入变量。例如:
假设有两个组件:A和B,A是B组件的祖先组件

// A.vue
export default {
  provide: {
    name: '天涯'
  }
}
// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // 天涯
  }
}

我们可以看到在祖先组件A中提供了一个变量,那么在其所有的后代组件中都可以注入这个变量并使用。

所以,上面 A.vue 的 name 如果改变了,B.vue 的 this.name 是不会改变的,仍然是天涯。

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里

然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用$root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像Vuex这样真正的状态管理方案了。

非关联组件通信

vuex

  vuex想必大家都非常熟悉了,它是vue的状态管理管理中心,存储的数据是响应式的,但并不会保存起来,刷新之后就回到了初始状态,具体做法应该在vuex数据发生改变的时候把数据拷贝一份保存到localStorage里面,这样子也可以实现实现父子组件、兄弟组件、跨级组件、非关联组件等数据通信。在这里也不再举例。

eventBus

  它的实现思想也很好理解,在要互相通信的两个组件中,都引入同一个新的vue实例,然后在两个组件中通过分别调用这个实例的事件触发和监听来实现通信。

//eventBus.js
import Vue from 'vue';  
export default new Vue();
<!--组件A-->
<script>
import Bus from 'eventBus.js'; 
export default {
    methods: {  
        sayHello() {  
            Bus.$emit('sayHello', 'hello');   
        }  
    } 
}
</script>
<!--组件B-->
<script>
import Bus from 'eventBus.js'; 
export default {
    created() {  
        Bus.$on('sayHello', target => {  
            console.log(target);  // => 'hello'
        });  
    } 
}
</script>

$root

  通过$root根组件任何组件都可以获取当前组件树的根vue实例,通过维护根实例上的data,就可以实现组件间的数据共享。

// 组件A
<script>
export default {
    created() {  
        this.$root.$emit('changeTitle', '我是A')
    }
}
</script>
// 组件B
<script>
export default {
    created() {  
        this.$root.$on('changeTitle')
    },
    methods: {
      changeTitle (title)  {
         console.log(title)
      }
   }
}
</script>

这种方式有个弊端就是组件A和组件B必须同时存在。下面用另外一种方式可以优化:

//main.js 根实例
new Vue({
    el: '#app',
    store,
    router,
    // 根实例的 data 属性,维护通用的数据
    data: function () {
        return {
            author: ''
        }
    }, 
    components: { App },
    template: '<App/>',
});
<!--组件A-->
<script>
export default {
    created() {  
        this.$root.author = '于是乎'
    }
}
</script>
<!--组件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>

  通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。

总结:
本文讲了组件之间的各种通信:
父子组件通信:prop和$emit、v-model、.sync、$parent、children和ref

父子组件双向绑定:props和$emit、v-model、.sync

跨级组件通信:$attrs和$listeners、provide和inject

非关联组件通信:vuex、eventBus、$root

参考文章:https://juejin.im/post/5d0489dff265da1ba84a8e41

https://juejin.im/post/5cde0b43f265da03867e78d3

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Vue 实例 属性和方法 每个 Vue 实例都会代理其 data 对象里所有的属性:var data = { a:...
    云之外阅读 2,198评论 0 6
  • 通信方式: 一、props 当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问...
    lyy94阅读 510评论 0 0
  • Vue组件的通信方式大致有这11(12)种 常用的Props $attrs & $listeners provid...
    zpkzpk阅读 1,030评论 0 3
  • 一、了解Vue.js 1.1.1 Vue.js是什么? 简单小巧、渐进式、功能强大的技术栈 1.1.2 为什么学习...
    蔡华鹏阅读 3,311评论 0 3
  • 北京的雨滴 2018-06-14 北京的雨滴感恩日记第954天 1、感恩今天是周四,今天早上我不用早起,上午请假了...
    北京的雨滴阅读 90评论 0 0