Vue组件间通信详解

    众所周知,组件是 vue.js最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用。那么组件之间如何通信呢?主要有,props$emit/$onvuex$parent / $children$attrs/$listenersprovide/inject,本文将分别介绍组件间的通信方式。

image.png

上图表示了组件之间所有可能的关系
A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)

第一种 props/$emit,适用于父子组件之间的通信

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app">
      <h3>组件间的通信(父子组件之间的通信props和$emit)</h3>
      <parent></parent>
    </div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('parent', {
      template: `
        <div>
          <h2>父组件</h2>
          <p>子组件传来的数据==>{{ fromChildMsg }}</p>
          <child :msgParent="msgParent" @sendMsgToParent="childMsgHandle"></child>  
        </div>
      `,
      data(){
        return {
          msgParent:'父组件的数据',
          fromChildMsg:''
        }
      },
      methods: {
        childMsgHandle(msg){
          this.fromChildMsg = msg
        }
      }
    })
    Vue.component('child', {
      template: `
        <div>
          <h3>子组件</h3>
          <p>父组件传来的数据==>{{ msgParent }}</p>
        </div>
      `,
      props:{
        msgParent:{
          type:String
        }
      },
      data() {
        return {
          msgChild: '子组件的数据'
        }
      },
      mounted () {
        this.$emit('sendMsgToParent',this.msgChild)
      }
    })
    const app = new Vue({
      el: '#app',
      data: {}
    })
  </script>
</html>

注意:在组件间通行过程中,Vue组件遵循单向数据流原则:就是数据只能通过 props 由父组件流向子组件,而子组件并不能通过修改 props 传过来的数据修改父组件的相应状态。原因是:所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

运行结果

image.png

第二种,$attrs$listeners,适用于后代组件的通信

  • $attrs:包含了父作用域中不被prop 所识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定属性 (classstyle 除外),并且可以通过 v-bind="$attrs" 传入内部组件。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app">
      <h3>组件间的通信(父组件和后代之间的通信$attrs和$listeners)</h3>
      <parent />
    </div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('parent', {
      template: `
        <div>
          <h2>父组件</h2>
          <p>子组件传来的数据==>{{ fromChildMsg }}</p>
          <p>后代件传来的数据==>{{ fromProgenyMsg }}</p>
          <child :msgParentToProgeny="msgParentToProgeny" :msgParent="msgParent" @sendMsgToParent="childMsgHandle" @progenyToParent="progenyMsgHandle"/>
        </div>
      `,
      data(){
        return {
          msgParent:'父组件的数据',
          msgParentToProgeny:'父组件数据给后代组件',
          fromChildMsg:'',
          fromProgenyMsg:''
        }
      },
      methods: {
        childMsgHandle(msg){
          this.fromChildMsg = msg
        },
        progenyMsgHandle(msg){
          this.fromProgenyMsg = msg
        }
      }
    })
    Vue.component('child', {
      template: `
        <div>
          <h3>子组件</h3>
          <p>父组件传来的数据==>{{ msgParent }}</p>
          //  后代组件中能直接触发 progenyToParent 的原因在于:子组件调用 后代组件时,使用 v-on 绑定了 $listeners 属性 
          //  通过v-bind 绑定 $attrs 属性,后代组件可以直接获取到 父组件组件中传递下来的 props(除了 子组件中 props声明的) 
          <progeny v-bind="$attrs" v-on="$listeners" />
        </div>
      `,
      props:{
        msgParent:{
          type:String
        }
      },
      data() {
        return {
          msgChild: '子组件的数据'
        }
      },
      mounted () {
        this.$emit('sendMsgToParent',this.msgChild)
      }
    })
    Vue.component('progeny',{
      template:`
        <div>
          <h3>后代组件</h3>
        </div>
      `,
      data(){
        return {
          progenyMsg:'后代组件的信息'
        }
      },
      mounted () {
        this.$emit('progenyToParent',this.progenyMsg)
      }
    })
    const app = new Vue({
      el: '#app',
      data: {}
    })
  </script>
</html>

运行结果

image.png

第三种:事件总线方式,在项目规模不大的情况下,完全可以使用中央事件总线 的方式,中央事件总线 EventBus 非常简单,就是任意组件和组件之间打交道,没有多余的业务逻辑,只需要在状态变化组件触发一个事件,然后在处理逻辑组件监听该事件就可以。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app"></div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('child1', {
      template: `
        <div>
          <h3>子组件1</h3>  
          <button @click="$EventBus.$emit('child1Event',child1Msg)">bus</button>
        </div>
      `,
      data() {
        return {
          child1Msg:'组件1的信息'
        }
      }
    })
    Vue.component('child2', {
      template: `
        <div>
          <h3>子组件2</h3>  
        </div>
      `,
      mounted () {
        this.$EventBus.$on('child1Event',msg=>{
          console.log(msg)
        })
      }
    })
    // 定义事件总线
    const EventBus = new Vue()
    Vue.prototype.$EventBus = EventBus
    const app = new Vue({
      el: '#app',
      template: `
        <div>
          <h3>组件间的通信(父子组件之间的通信-事件总线)</h3>
          <child1 />
          <child2 />
        </div>
      `
    })
  </script>
</html>

image.png

第四种,provideinject,适用于根组件和后代组件通信

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app">
      <c1 />
    </div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('c1', {
      template: `
        <div>
          <h3>组件1</h3> 
          <p>组件1-{{ rootMsg }}</p>
          <c11 /> 
        </div>
      `,
      provide: {
        c1Msg: '组件1信息'
      },
      inject: ['rootMsg']
    })
    Vue.component('c11', {
      template: `
        <div>
          <h3>组件1-1</h3>  
          <p>组件1-1-{{ c1Msg }}</p>
          <c111 />
        </div>
      `,
      inject: ['c1Msg']
    })
    Vue.component('c111', {
      template: `
        <div>
          <h3>组件1-1-1</h3>
          <p>组件1-1-1{{ c1Msg }}</p> 
        </div>
      `,
      inject: ['c1Msg']
    })
    const app = new Vue({
      el: '#app',
      provide: {
        rootMsg: '根信息'
      }
    })
  </script>
</html>
image.png

第五种 v-model,适用于父子组件的通信

v-model用于父子组件之间的通信,体现的是v-model的实现原理:绑定value,监听input事件。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app">
      <parent />
    </div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('parent',{
      template:`
        <div>
          <h3>父组件</h3>  
          <p>父组件==>{{ message }}</p>
          <child v-model="message" />
        </div>
      `,
      data(){
        return {
          message:''
        }
      }
    })
    Vue.component('child',{
      template:`
        <div>
          <input type="text" v-model="msg" @input="changeValueHandle" /> 
        </div>
      `,
      props:{
        value:{
          type:String //v-model 会自动传递一个字段为 value 的 props 属性
        }
      },
      data(){
        return {
          msg:''
        }
      },
      methods:{
        changeValueHandle(){
          this.$emit('input',this.msg)
        }
      }
    })
    const app = new Vue({
      el: '#app'
    })
  </script>
</html>
image.png

第六种 $parent$children,适用于父子组件之间的通信

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Vue组件间的通信</title>
  </head>
  <body>
    <div id="app">
      <parent />
    </div>
  </body>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
  <script>
    Vue.component('parent', {
      template: `
        <div>
          <h2>父组件</h2>  
          {{ childMsgFromParent }}
          <button @click="getChildMsgHandle">获取子组件信息</button>
          <child />
        </div>
      `,
      data() {
        return {
          parentMsg: '父组件的信息',
          childMsgFromParent:''
        }
      },
      methods: {
        getChildMsgHandle() {
          this.childMsgFromParent = this.$children[0].childMsg
          console.log(this.$children[0].childMsg)
        }
      }
    })
    Vue.component('child', {
      template: `
        <div>
          <h3>子组件</h3>  
          {{ parentMsgFromChild }}
          <button @click="getParentMsgHandle">获取父组件信息</button>
        </div>
      `,
      data() {
        return {
          childMsg: '子组件信息',
          parentMsgFromChild:''
        }
      },
      methods: {
        getParentMsgHandle() {
          this.parentMsgFromChild = this.$parent.parentMsg
          console.log(this.$parent.parentMsg)
        }
      }
    })
    const app = new Vue({
      el: '#app'
    })
  </script>
</html>
image.png

第七种 Vuex,全局状态管理,这里暂不做介绍。详情请看Vuex

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

推荐阅读更多精彩内容