vue 组件通信总结

时间:2021-2-20 作者:admin

1.props 和 $emit

用过 Vue 技术栈开发项目过的开发者对这样一个组合肯定不会陌生,这种组件通信的方式是我们运用的非常多的一种。props 以单向数据流的形式可以很好的完成父子组件的通信。

所谓单向数据流:就是数据只能通过 props 由父组件流向子组件,而子组件并不能通过修改 props 传过来的数据修改父组件的相应状态。至于为什么这样做,Vue 官网做出了解释:

所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

额外的,每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。

正因为这个特性,于是就有了对应的 emit。emit。emitemit 用来触发当前实例上的事件。对此,我们可以在父组件自定义一个处理接受变化状态的逻辑,然后在子组件中如若相关的状态改变时,就触发父组件的逻辑处理事件。

// 父组件
Vue.component('parent', {
  template:`
    <div>
      <p>this is parent component!</p>
      <child :message="message" v-on:getChildData="getChildData"></child>
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  },
  methods:{
    // 执行子组件触发的事件
    getChildData(val) {
      console.log(val);
    }
  }
});

// 子组件
Vue.component('child', {
  template:`
    <div>
      <input type="text" v-model="myMessage" @input="passData(myMessage)">
    </div>
  `
  props: {
    message: {
        type: String,
        default: ''
    }
  }

  data() {
    return {
      // 这里是必要的,因为你不能直接修改 props 的值
      myMessage: this.message
    }
  },

  methods:{
    passData(val) {
      // 数据状态变化时触发父组件中的事件
      this.$emit('getChildData', val);
    }
  }
});

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

在上面的例子中,有父组件 parent 和子组件 child。

1)、 父组件传递了 message 数据给子组件,并且通过v-on绑定了一个 getChildData 事件来监听子组件的触发事件;

2)、 子组件通过 props 得到相关的 message 数据,然后将数据缓存在 data 里面,最后当属性数据值发生变化时,通过 this.$emit 触发了父组件注册的 getChildData 事件处理数据逻辑。

2、attrs和attrs 和 attrslisteners

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

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

// 组件A
Vue.component('A', {
  template: `
    <div>
      <p>this is parent component!</p>
      <B :messagec="messagec" :message="message" v-on:getCData="getCData" v-on:getChildData="getChildData(message)"></B>
    </div>
  `,
  data() {
    return {
      message: 'hello',
      messagec: 'hello c' //传递给c组件的数据
    }
  },
  methods: {
    // 执行B子组件触发的事件
    getChildData(val) {
      console.log(`这是来自B组件的数据:${val}`);
    },

    // 执行C子组件触发的事件
    getCData(val) {
      console.log(`这是来自C组件的数据:${val}`);
    }
  }
});

// 组件B
Vue.component('B', {
  template: `
    <div>
      <input type="text" v-model="mymessage" @input="passData(mymessage)"> 
      <!-- C组件中能直接触发 getCData 的原因在于:B组件调用 C组件时,使用 v-on 绑定了 $listeners 属性 -->
      <!-- 通过v-bind 绑定 $attrs 属性,C组件可以直接获取到 A组件中传递下来的 props(除了 B组件中 props声明的) -->
      <C v-bind="$attrs" v-on="$listeners"></C>
    </div>
  `
  props: {
     message: {
       type: String,
       default: ''
     }
  },

  data(){
    return {
      mymessage: this.message
    }
  },

  methods: {
    passData(val){
      //触发父组件中的事件
      this.$emit('getChildData', val)
    }
  }
});

// 组件C
Vue.component('C', {
  template: `
    <div>
      <input type="text" v-model="$attrs.messagec" @input="passCData($attrs.messagec)">
    </div>
  `,
  methods: {
    passCData(val) {
      // 触发父组件A中的事件
      this.$emit('getCData',val)
    }
  }
});

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

在上面的例子中,我们定义了 A,B,C 三个组件,其中组件B 是组件 A 的子组件,组件C 是组件B 的子组件。

1). 在组件 A 里面为组件 B 和组件 C 分别定义了一个属性值(message,messagec)和监听事件(getChildData,getCData),并将这些通过 props 传递给了组件 A 的直接子组件 B;

2). 在组件 B 中通过 props 只获取了与自身直接相关的属性(message),并将属性值缓存在了 data 中,以便后续的变化监听处理,然后当属性值变化时触发父组件 A 定义的数据逻辑处理事件(getChildData)。关于组件 B 的直接子组件 C,传递了属性 attrs和绑定了事件attrs 和绑定了事件 attrslisteners;

3). 在组件 C 中直接在 v-model 上绑定了 attrs属性,通过v−on绑定了attrs 属性,通过 v-on 绑定了 attrsvonlisteners;

最后就将 attrs和attrs 和 attrslisteners 单独拿出来说说吧!

attrs:包含了父作用域中不被prop所识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定属性(class和style除外),并且可以通过v−bind=”attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定属性 (class和 style 除外),并且可以通过 v-bind=”attrsprop()(classstyle)prop(classstyle)vbind=attrs” 传入内部组件。

listeners:包含了父作用域中的(不含.native修饰器的)v−on事件监听器。它可以通过v−on=”listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”listeners(.native)vonvon=listeners” 传入内部组件。

3、中央事件总线 EventBus

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

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

// 组件 A
Vue.component('A', {
  template: `
    <div>
      <p>this is A component!</p>
      <input type="text" v-model="mymessage" @input="passData(mymessage)"> 
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother1'
    }
  },
  methods: {
    passData(val) {
      //触发全局事件globalEvent
      this.$EventBus.$emit('globalEvent', val)
    }
  }
});

// 组件 B
Vue.component('B', {
  template:`
    <div>
      <p>this is B component!</p>
      <p>组件A 传递过来的数据:{{brothermessage}}</p>
    </div>
  `,
  data() {
    return {
      mymessage: 'hello brother2',
      brothermessage: ''
    }
  },
  mounted() {
    //绑定全局事件globalEvent
    this.$EventBus.$on('globalEvent', (val) => {
      this.brothermessage = val;
    });
  }
});

//定义中央事件总线
const EventBus = new Vue();

// 将中央事件总线赋值到 Vue.prototype 上,这样所有组件都能访问到了
Vue.prototype.$EventBus = EventBus;

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

在上面的实例中,我们定义了组件 A 和组件 B,但是组件 A 和组件 B 之间没有任何的关系。

1)、 首先我们通过 new Vue() 实例化了一个 Vue 的实例,也就是我们这里称呼的中央事件总线 EventBus ,然后将其赋值给了 Vue.prototype.$EventBus,使得所有的业务逻辑组件都能够访问到;

2)、 然后定义了组件 A,在组件 A 里面定义了一个处理的方法 passData,主要定义触发一个全局的 globalEvent 事件,并传递一个参数;

3). 最后定义了组件 B,在组件 B 里面的 mounted 生命周期监听了组件 A 里面定义的全局 globalEvent 事件,并在回调函数里面执行了一些逻辑处理。

中央事件总线 EventBus 非常简单,就是任意组件和组件之间打交道,没有多余的业务逻辑,只需要在状态变化组件触发一个事件,然后在处理逻辑组件监听该事件就可以。该方法非常适合小型的项目!

4、provide 和 inject

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

// 定义 parent 组件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <child></child>
    </div>
  `,
  provide: {
    for:'test'
  },
  data() {
    return {
      message: 'hello'
    }
  }
});

// 定义 child 组件
Vue.component('child', {
  template: `
    <div>
      <input type="tet" v-model="mymessage"> 
    </div>
  `,
  inject: ['for'],    // 得到父组件传递过来的数据
  data(){
    return {
      mymessage: this.for
    }
  },
});

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

在上面的实例中,我们定义了组件 parent 和组件 child,组件 parent 和组件 child 是父子关系。

1)、 在 parent 组件中,通过 provide 属性,以对象的形式向子孙组件暴露了一些属性

2)、 在 child 组件中,通过 inject 属性注入了 parent 组件提供的数据,实际这些通过 inject 注入的属性是挂载到 Vue 实例上的,所以在组件内部可以通过 this 来访问。

⚠️ 注意:官网文档提及 provide 和 inject 主要为高阶插件/组件库提供用例,并不推荐直接用于应用程序代码中。

5、parent和parent 和 parentchildren

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

// 定义 parent 组件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <button @click="changeChildValue">test</button>
      <child />
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  },
  methods: {
    changeChildValue(){
      this.$children[0].mymessage = 'hello';
    }
  },
});

// 定义 child 组件
Vue.component('child', {
  template:`
    <div>
      <input type="text" v-model="mymessage" @change="changeValue" /> 
    </div>
  `,
  data() {
    return {
      mymessage: this.$parent.message
    }
  },
  methods: {
    changeValue(){
      this.$parent.message = this.mymessage;//通过如此调用可以改变父组件的值
    }
  },
});

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

在上面实例代码中,分别定义了 parent 和 child 组件,这两个组件是直接的父子关系。两个组件分别在内部定义了自己的属性。在 parent 组件中,直接通过 this.children[0].mymessage=′hello′;给child组件内的mymessage属性赋值,而在child子组件中,同样也是直接通过this.children[0].mymessage = ‘hello’; 给 child 组件内的 mymessage 属性赋值,而在 child 子组件中,同样也是直接通过this.children[0].mymessage=hello;childmymessagechildthis.parent.message 给 parent 组件中的 message 赋值,形成了父子组件通信。

$children 是一个数组,是直接儿子的集合,关于具体是第几个儿子,那么儿子里面有个 _uid 属性,可以知道他是第几个元素,是元素的唯一标识符,根据这个属性,我们可以进行其他的操作

6、v-model

说到 v-model 这个指定,大家肯定会想到双向数据绑定,如 input 输入值,下面的显示就是实时的根据输入的不同而显示相应的内容。

// 定义 parent 组件
Vue.component('parent', {
  template: `
    <div>
      <p>this is parent component!</p>
      <p>{{message}}</p>
      <child v-model="message"></child>
    </div>
  `,
  data() {
    return {
      message: 'hello'
    }
  }
});

// 定义 child 组件
Vue.component('child', {
  template: `
    <div>
      <input type="text" v-model="mymessage" @change="changeValue"> 
    </div>
  `,
  props: {
    value: String, // v-model 会自动传递一个字段为 value 的 props 属性
  },
  data() {
    return {
      mymessage: this.value
    }
  },
  methods: {
    changeValue() {
      this.$emit('input', this.mymessage); //通过如此调用可以改变父组件上 v-model 绑定的值
    }
  },
});

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

在上面的实例代码中,我们定义了 parent 和 child 两个组件,这两个组件是父子关系,v-model 也只能实现父子组件之间的通信。

1)、 在 parent 组件中,我们给自定义的 child 组件实现了 v-model 绑定了 message 属性。此时相当于给 child 组件传递了 value 属性和绑定了 input 事件。

2)、 顺理成章,在定义的 child 组件中,可以通过 props 获取 value 属性,根据 props 单向数据流的原则,又将 value 缓存在了 data 里面的 mymessage 上,再在 input 上通过 v-model 绑定了 mymessage 属性和一个 change 事件。当 input 值变化时,就会触发 change 事件,处理 parent 组件通过 v-model 给 child 组件绑定的 input 事件,触发 parent 组件中 message 属性值的变化,完成 child 子组件改变 parent 组件的属性值。

这里主要是 v-model 的实现原理要着重了解一下!这种方式的用处适合于将展示组件和业务逻辑组件分离。

7、.sync

此方法其实用的也不少,它在 Vue 1.x 里的作用是对一个 prop 进行“双向绑定“。但在 Vue 2 之后是只允许单向数据流的,所以现在即使它看起来像是真正的“双向绑定”,本质上也只是作为一个编译时的语法糖存在而已。

举个计数器的例子:

// 父组件

<template>
  <div>
    {{num}}
   <child-a :count.sync="num" />
  </div>
</template>

<script>
import childA from "../components/ChildA";
export default {
  data() {
    return {
      num: 0
    };
  },
  components: { childA }
};
</script>


// 子组件

<template>
  <div>
     <div @click="handleAdd">ADD</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      counter: this.count
    };
  },
  props: ["count"],
  methods: {
    handleAdd() {
      this.$emit("update:count", ++this.counter);
    }
  }
};
</script>
声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。