1
序言:我们都知道vue父子组件通信主要通过props和事件,那还知道其他形式的通讯方式吗?本文将一一为你揭晓。

1、sync修饰符 + this.$emit('update:属性名', data)(父子组件)

1.1、sync修饰符的作用

在Vue中,子父组件最常用的通信方式就是通过props进行数据传递,props值只能在父组件中更新并传递给子组件,在子组件内部,是不允许改变传递进来的props值,这样做是为了保证数据单向流通。但有时候,我们会遇到一些场景,需要在子组件内部改变props属性值并更新到父组件中,这时就需要用到.sync修饰符。

如果我们在子组件中直接修改props中的属性值,将会报以下错误:

图片描述

报错的大概意思就是:不允许直接修改props里面的属性值。

这里可以参考一篇文章:https://blog.csdn.net/XuM2222...

1.2、利用sync修饰符实现双向数据绑定

父组件:通过给绑定属性添加sync修饰符将值传递给子组件,父组件值改变,子组件随着改变。
子组件:通过this.$emit('update:属性名', data)来改变props属性值并更新父组件对应的值。

<template>
  <div @click="click2change">
    点击加一{{foo}}
    <my-checkbox :checked.sync="foo"></my-checkbox>
  </div>
</template>

<script>
import Vue from 'vue';
// 组件通信: props,事件,provide|inject,vuex, vuebus,。。。

const myCheckbox = Vue.component('my-checkbox', {
  props: {
    value: {
      type: String,
      default: 'testvalue'
    },
    checked: {
      type: Number,
      default: 0
    }
  },
  methods: {
    changeValue() {
      //this.checked -= 2;//会报刚刚说的错误,不能直接修改props属性值
      this.$emit("update:checked", this.checked - 2);
    }
  },
  template: `
    <div>
      <div @click.stop="changeValue">checked: {{checked}}</div>
    </div>
  `
});

export default {
  name: 'list',
  components: {
    myCheckbox,
  },
  data() {
    return {
      foo: 1
    }
  },
  methods: {
    click2change() {
      this.foo += 1;
    }
  }
}
</script>

结果运行:
图片描述

2、自定义组件v-model+this.$emit('自定事件名', data)(父子组件)

2.1、v-model语法糖

图片描述

2.2、自定义组件v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value 特性用于不同的目的。model 选项可以用来避免这样的冲突:

Vue.component('base-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    checked: Boolean
  },
  template: `
    <input
      type="checkbox"
      v-bind:checked="checked"
      v-on:change="$emit('change', $event.target.checked)"
    >
  `
})

现在在这个组件上使用 v-model 的时候:

<base-checkbox v-model="lovingVue"></base-checkbox>

这里的 lovingVue 的值将会传入这个名为 checked 的 prop。同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的属性将会被更新。

注意:你仍然需要在组件的 props 选项里声明 checked 这个 prop。

2.3、使用自定义组件v-model实现双向数据绑定

父组件:通过v-model向子组件传值,并监听自定义函数更新值。
子组件:通过model选项自定义绑定属性名和方法,使用$emit触发自定义方法更新父组件的值。

<template>
  <div @click="click2change">
    点击加一{{foo}}
    <my-checkbox v-model="foo"></my-checkbox>
  </div>
</template>

<script>
import Vue from 'vue';

// 组件通信: props,事件,provide|inject,vuex, vuebus,。。。

const myCheckbox = Vue.component('my-checkbox', {
  model: {
    prop: 'checked',
    event: 'change'
  },
  props: {
    //需要在组件的 props 选项里声明 checked 这个 prop
    checked: {
      type: Number,
      default: 0
    }
  },
  methods: {
    changeValue() {
      this.$emit("change", this.checked - 1);
    }
  },
  template: `
    <div>
      <div @click.stop="changeValue">checked: {{checked}}</div>
    </div>
  `
});

export default {
  name: 'list',
  components: {
    myCheckbox,
  },
  data() {
    return {
      foo: 1
    }
  },
  methods: {
    click2change() {
      // debugger
      this.foo += 1;
    }
  }
}
</script>

运行结果:

图片描述

3、value方式+this.$emit('input', data)(父子组件)

这种方式实现双向数据绑定是基于v-model语法糖

<template>
  <div @click="click2change">
    点击加一{{foo}}
    <my-checkbox v-model="foo"></my-checkbox>
  </div>
</template>

<script>
import Vue from 'vue';

// 组件通信: props,事件,provide|inject,vuex, vuebus,。。。

const myCheckbox = Vue.component('my-checkbox', {
  props: {
    value: {
      type: Number,
      default: 0
    }
  },
  methods: {
    changeValue() {
      this.$emit("input", this.value - 1);
    }
  },
  template: `
    <div>
      <div @click.stop="changeValue">value: {{value}}</div>
    </div>
  `
});

export default {
  name: 'list',
  components: {
    myCheckbox,
  },
  data() {
    return {
      foo: 1
    }
  },
  methods: {
    click2change() {
      // debugger
      this.foo += 1;
    }
  }
}
</script>

运行结果:
图片描述

4、props + $emit(父子组件)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>props + $emit</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <!-- props + $emit -->
  <div id="app"></div>
</body>
</html>

<script>
  Vue.component('A', {
    template: `
      <div>
        <span>{{ message }}</span>  
        <B :message="message" @messageListener="messageListener"></B>
      </div>
    `,
    data() {
      return {
        message: '123'
      }
    },
    methods: {
      messageListener(val) {
        this.message = val
      }
    }
  })
  Vue.component('B', {
    template: `
      <input type="text" v-model="myMessage" @input="messageChange(myMessage)"/>
    `,
    data() {
      return {
        // 这里是必要的,因为你不能直接修改 props 的值
        myMessage: this.message
      }
    },
    props: {
      message: {
        type: String,
        default: ''
      }
    },
    methods: {
      messageChange(val) {
        this.$emit('messageListener', val)
      }
    }
  })

  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

5、$attrs + $listeners(祖孙组件)

上面这种组件通信的方式只适合直接的父子组件,也就是如果父组件A下面有子组件B,组件B下面有组件C,这时如果组件A直接想传递数据给组件C那就行不通了! 只能是组件A通过 props 将数据传给组件B,然后组件B获取到组件A 传递过来的数据后再通过 props 将数据传给组件C。当然这种方式是非常复杂的,无关组件中的逻辑业务一种增多了,代码维护也没变得困难,再加上如果嵌套的层级越多逻辑也复杂,无关代码越多!

针对这样一个问题,Vue 2.4 提供了$attrs$listeners 来实现能够直接让组件A传递消息给组件C。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>$attrs + $listeners</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

<script>
  Vue.component('A', {
    template: `
      <div>
        <span>{{ message + ' ' + name + ' ' + age + ' ' + school }}</span>  
        <B :message="message" :name="name" :age="age" school="school" @messageListener="messageListener" @nameListener="nameListener"></B>
      </div>
    `,
    data() {
      return {
        message: 'hello',
        name: 'tom',
        age: 20,
        school: '中南财经政法大学'
      }
    },
    methods: {
      messageListener(val) {
        this.message = val
      },
      nameListener(val) {
        this.name = val
      }
    }
  })
  Vue.component('B', {
    template: `
      <div>
        <input type="text" v-model="myMessage" @input="messageChange(myMessage)"> 
        <C v-bind="$attrs" v-on="$listeners"></C>
      </div>
    `,
    data() {
      return {
        // 这里是必要的,因为你不能直接修改 props 的值
        myMessage: this.message
      }
    },
    props: {
      message: {
        type: String,
        default: ''
      }
    },
    methods: {
      messageChange(val) {
        this.$emit('messageListener', val)
      }
    }
  })

  Vue.component('C', {
    template: `
      <div>
        <span>{{ $attrs.message }}<span>
        <span>{{ $attrs.age }}<span>
        <span>{{ $attrs.name }}<span>
        <span>{{ $attrs.school }}<span>
        <button @click="changeName">改变name</button>
      </div>
    `,
    methods: {
      // c组件的信息,怎么同步给a组件呢? 
      // 我们在b组件上 绑定 v-on=”$listeners”, 在a组件中,监听c组件触发的事件。就能把c组件发出的数据,传递给a组件。
      changeName() {
        this.$emit('nameListener', 'john')
      }
    }
  })
  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

$attrs$listeners 单独拿出来说说吧!

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定属性 (class和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件。

6、中央事件总线 EventBus(父子、祖孙、兄弟组件)

对于两个组件不是父子关系,那么又该如何实现通信呢?在项目规模不大的情况下,完全可以使用中央事件总线EventBus的方式。如果你的项目规模是大中型的,那你可以使用我们后面即将介绍的Vuex 状态管理

EventBus通过新建一个Vue事件bus对象,然后通过bus.$emit触发事件,bus.$on监听触发的事件。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>事件总线eventBus</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

<script>
  // 创建共享实例
  const eventBus = new Vue()
  Vue.prototype.$eventBus = eventBus

  Vue.component('A', {
    template: `
      <div>
        <B></B>
        <C></C>
      </div>
    `,
    data() {
      return {}
    },
  })
  Vue.component('B', {
    template: `
      <div>
        <p>{{ message }}</p>
        <button @click="sendC">发送给C</button>
        <p>===================================</p>
      </div>
    `,
    data() {
      return {
        message: ''
      }
    },
    mounted() {
      this.$eventBus.$on('recieve', val => {
        this.message = val
      })
    },
    methods: {
      sendC() {
        this.message = '我是B,C收到请回答'
        this.$eventBus.$emit('send', this.message)
      }
    }
  })

  Vue.component('C', {
    template: `
      <div>
        <span>{{ message }}</span>
        <button @click="sendB">发送给B</button>
      </div>
    `,
    data() {
      return {
        message: ''
      }
    },
    mounted() {
      this.$eventBus.$on('send', val => {
        this.message = 'C收到了'
      })
    },
    methods: {
      sendB() {
        this.$eventBus.$emit('recieve', 'B,我们多久没见了?')
      }
    }
  })
  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

7、provide + inject(父子、祖孙)

关于provide/inject介绍:https://segmentfault.com/a/11...

熟悉 React 开发的同学对 Context API 肯定不会陌生吧!在 Vue 中也提供了类似的 API 用于组件之间的通信。

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

⚠️ 注意:官网文档提及 provide 和 inject 主要为高阶插件/组件库提供用例(比如elementui),并不推荐直接用于应用程序代码中。
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>provide + inject</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

<script>
  Vue.component('A', {
    template: `
      <div>
        <B></B>
      </div>
    `,
    provide: {
      msg: '1234124'
    }
  })
  Vue.component('B', {
    template: `
      <div>
        <label>B:</label>
        <span>{{ this.msg }}</span>
        <C></C>
      </div>
    `,
    provide: {
      msg: '42341234',
      name: 'asdasda'
    },
    inject: ['msg'],
  })

  Vue.component('C', {
    template: `
      <div>
        <label>C:</label>
        <span>{{ this.xingming }}</span>
        <span>{{ this.msg }}</span>
      </div>
    `,
    inject: {
      xingming: {
        from: 'name',
        default: ''
      },
      msg: {
        from: 'msg',
        default: ''
      }
    },
    data() {
      return {
      }
    },
  })
  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

8、$parent + $children

这里要说的这种方式就比较直观了,直接操作父子组件的实例。$parent就是父组件的实例对象,而$children就是当前实例的直接子组件实例了,不过这个属性值是数组类型的,且并不保证顺序,也不是响应式的。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>$parent + $children</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

<script>
  Vue.component('A', {
    template: `
      <div>
        <span>{{ msg }}</span>
        <button @click="changeB">changeB</button>
        <B></B>
      </div>
    `,
    data() {
      return {
        msg: 'hello, world'
      }
    },
    methods: {
      changeB() {
        // 操作子组件实例,$children是一个数组
        this.$children[0].msg = 'hello, world'
      }
    },
  })
  Vue.component('B', {
    template: `
      <div>
        <span>{{ msg }}</span>
        <button @click="changeA">changeA</button>
      </div>
    `,
    data() {
      return {
        msg: ''
      }
    },
    methods: {
      changeA() {
        // 操作父组件实例
        this.$parent.msg = 'welcome, you'
      }
    }
  })
  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

9、ref

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ref</title>
  <script src="https://cdn.bootcss.com/vue/2.6.11/vue.min.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

<script>
  Vue.component('A', {
    template: `
      <div>
        <button @click="changeB">changeB</button>
        <B ref="comB"></B>
      </div>
    `,
    methods: {
      changeB() {
        this.$refs.comB.msg = 'welcome, you'
      }
    },
  })
  Vue.component('B', {
    template: `
      <div>
        <span>{{ msg }}</span>
      </div>
    `,
    data() {
      return {
        msg: 'hello, world'
      }
    },
  })
  var app=new Vue({
    el: '#app',
    template: `
      <div>
        <A />
      </div>
    `
  });
</script>

参考:
https://blog.csdn.net/songxiu...
https://juejin.im/post/5c77c4...


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。


下一篇 »
深度理解slot