**

<table><tbody><tr><td bgcolor="#FDFFE7"><font size="4">原创不易,希望能关注下我们,再顺手点个赞~~<font></font></font></td></tr></tbody></table> **

> 本文首发于政采云前端团队博客: Vue 组件数据通信方案总结open in new window

季节.png

背景

初识 Vue.js ,了解到组件是 Vue 的主要构成部分,但组件内部的作用域是相对独立的部分,组件之间的关系一般如下图:

组件 A 与组件 B 、C 之间是父子组件,组件 B 、C 之间是兄弟组件,而组件 A 、D 之间是隔代的关系。

那么对于这些不同的关系,本文主要分享了他们之间可以采用的几种数据通信方式,例如 Props 、$emit / $on 、Vuex 等,大家可以根据自己的使用场景可以选择适合的使用方式。

一、 Prop / $emit

1、Prop 是你可以在组件上注册的一些自定义特性。当一个值传递给一个 Prop 特性的时候,它就变成了那个组件实例的一个属性。父组件向子组件传值,通过绑定属性来向子组件传入数据,子组件通过 Props 属性获取对应数据

// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;child :title=&quot;title&quot;&gt;&lt;/child&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Child from &quot;./component/child.vue&quot;;
export default {
  name: &quot;demo&quot;,
  data: function() {
    return {
      title: &quot;我是父组件给的&quot;
    };
  },
  components: {
    Child
  },
};
&lt;/script&gt;
// 子组件
&lt;template&gt;
  &lt;div class=&quot;text&quot;&gt;{{title}}&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo&#39;,
  data: function() {},
  props: {
    title: {
      type: String
    }
  },
};
&lt;/script&gt;

2、$emit 子组件向父组件传值(通过事件形式),子组件通过 $emit 事件向父组件发送消息,将自己的数据传递给父组件。

// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;div class=&quot;title&quot;&gt;{{title}}&lt;/div&gt;
    &lt;child @changeTitle=&quot;parentTitle&quot;&gt;&lt;/child&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Child from &quot;./component/child.vue&quot;;

export default {
  name: &quot;demo&quot;,
  data: function() {
    return {
      title: null
    };
  },
  components: {
    Child
  },
  methods: {
    parentTitle(e) {
      this.title = e;
    }
  }
};
&lt;/script&gt;
// 子组件
&lt;template&gt;
  &lt;div class=&quot;center&quot;&gt;
    &lt;button @click=&quot;childTitle&quot;&gt;我给父组件赋值&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo&#39;,
  data() {
    return {
      key: 1
    };
  },
  methods: {
    childTitle() {
      this.$emit(&#39;changeTitle&#39;, `我给父组件的第${this.key}次`);
      this.key++;
    }
  }
};
&lt;/script&gt;

小总结:常用的数据传输方式,父子间传递。

二、 $emit / ​$on

这个方法是通过创建了一个空的 vue 实例,当做 $emit 事件的处理中心(事件总线),通过他来触发以及监听事件,方便的实现了任意组件间的通信,包含父子,兄弟,隔代组件。

// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;child1 :Event=&quot;Event&quot;&gt;&lt;/child1&gt;
    &lt;child2 :Event=&quot;Event&quot;&gt;&lt;/child2&gt;
    &lt;child3 :Event=&quot;Event&quot;&gt;&lt;/child3&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Vue from &quot;vue&quot;;

import Child1 from &quot;./component/child1.vue&quot;;
import Child2 from &quot;./component/child2.vue&quot;;
import Child3 from &quot;./component/child3.vue&quot;;

const Event = new Vue();

export default {
  name: &quot;demo&quot;,
  data: function() {
    return {
      Event: Event
    };
  },
  components: {
    Child1,
    Child2,
    Child3
  },
};
&lt;/script&gt;
// 子组件1
&lt;template&gt;
  &lt;div class=&quot;center&quot;&gt;
    1.我的名字是:{{name}}
    &lt;button @click=&quot;send&quot;&gt;我给3组件赋值&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &quot;demo1&quot;,
  data() {
    return {
      name: &quot;政采云&quot;
    };
  },
  props: {
    Event: Object
  },
  methods: {
    send() {
      this.Event.$emit(&quot;message-a&quot;, this.name);
    }
  }
};
&lt;/script&gt;
// 子组件2
&lt;template&gt;
  &lt;div class=&quot;center&quot;&gt;
    2.我的年龄是:{{age}}岁
    &lt;button @click=&quot;send&quot;&gt;我给3组件赋值&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
/* eslint-disable */
export default {
  name: &quot;demo2&quot;,
  data() {
    return {
      age: &quot;3&quot;
    };
  },
  props: {
    Event: Object
  },
  methods: {
    send() {
      this.Event.$emit(&quot;message-b&quot;, this.age);
    }
  }
};
&lt;/script&gt;
// 子组件3
&lt;template&gt;
  &lt;div class=&quot;center&quot;&gt;我的名字是{{name}},今年{{age}}岁&lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo3&#39;,
  data() {
    return {
      name: &#39;&#39;,
      age: &#39;&#39;
    };
  },
  props: {
    Event: Object
  },
  mounted() {
    this.Event.$on(&#39;message-a&#39;, name =&gt; {
      this.name = name;
    });
    this.Event.$on(&#39;message-b&#39;, age =&gt; {
      this.age = age;
    });
  },
};
&lt;/script&gt;

小总结:巧妙的在父子,兄弟,隔代组件中都可以互相数据通信。

三、 Vuex

Vuexopen in new window 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

Vuex实现了一个单项数据流,通过创建一个全局的 State 数据,组件想要修改 State 数据只能通过 Mutation 来进行,例如页面上的操作想要修改 State 数据时,需要通过 Dispatch (触发 Action ),而 Action 也不能直接操作数据,还需要通过 Mutation 来修改 State 中数据,最后根据 State 中数据的变化,来渲染页面。

// index.js
import Vue from &#39;vue&#39;;
import Tpl from &#39;./index.vue&#39;;
import store from &#39;./store&#39;;

new Vue({
  store,
  render: h =&gt; h(Tpl),
}).$mount(&#39;#app&#39;);
// store
import Vue from &#39;vue&#39;;
import Vuex from &#39;vuex&#39;;

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment(state) {
      state.count++;
    },
    reduce(state) {
      state.count--;
    }
  },
  actions: {
    actIncrement({ commit }) {
      commit(&#39;increment&#39;);
    },
    actReduce({ commit }) {
      commit(&#39;reduce&#39;);
    }
  },
  getters: {
    doubleCount: state =&gt; state.count*2
  }
});

export default store;
// vue文件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;p&gt;我的count:{{count}}&lt;/p&gt;
    &lt;p&gt;doubleCount:{{doubleCount}}&lt;/p&gt;
    &lt;button @click=&quot;this.actIncrement&quot;&gt;增加&lt;/button&gt;
    &lt;button @click=&quot;this.actReduce&quot;&gt;减少&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import { mapGetters, mapActions, mapState } from &quot;vuex&quot;;

export default {
  name: &quot;demo&quot;,
  data: function() {
    return {};
  },
  components: {},
  props: {},
  computed: {
    ...mapState([&quot;count&quot;]),
    ...mapGetters([&quot;doubleCount&quot;])
  },
  methods: {
    ...mapActions([&quot;actIncrement&quot;, &quot;actReduce&quot;])
  }
};
&lt;/script&gt;

Vuex 中需要注意的点:

Mutation :是修改State数据的唯一推荐方法,且只能进行同步操作。

Getter :Vuex 允许在Store中定义 "Getter"(类似于 Store 的计算属性)。Getter 的返回值会根据他的依赖进行缓存,只有依赖值发生了变化,才会重新计算。

本段只是简单介绍了一下 Vuex 的运行方式,更多功能例如 Module 模块请参考官网open in new window

小总结:统一的维护了一份共同的 State 数据,方便组件间共同调用。

四、 $attrs / $listeners

Vue 组件间传输数据在 Vue2.4 版本后有了新方法。除了 Props 外,还有了 $attrs /​ $listeners。

  • $attrs:包含了父作用域中不作为 Prop 被识别 (且获取) 的特性绑定 (ClassStyle 除外)。当一个组件没有声明任何 Prop 时,这里会包含所有父作用域的绑定 (ClassStyle 除外),并且可以通过 v-bind=&quot;$attrs&quot; 传入内部组件——在创建高级别的组件时非常有用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件

下面来看个例子

// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;button style=&quot;backgroundColor:lightgray&quot; @click=&quot;reduce&quot;&gt;减dd&lt;/button&gt;
    &lt;child1 :aa=&quot;aa&quot; :bb=&quot;bb&quot; :cc=&quot;cc&quot; :dd=&quot;dd&quot; @reduce=&quot;reduce&quot;&gt;&lt;/child1&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Child1 from &#39;./component/child1.vue&#39;;
export default {
  name: &#39;demo&#39;,
  data: function() {
    return {
      aa: 1,
      bb: 2,
      cc: 3,
      dd: 100
    };
  },
  components: {
    Child1
  },
  methods: {
    reduce() {
      this.dd--;
    }
  }
};
&lt;/script&gt;
// 子组件1
&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;center&quot;&gt;
      &lt;p&gt;aa:{{aa}}&lt;/p&gt;
      &lt;p&gt;child1的$attrs:{{$attrs}}&lt;/p&gt;
      &lt;button @click=&quot;this.reduce1&quot;&gt;组件1减dd&lt;/button&gt;
    &lt;/div&gt;
    &lt;child2 v-bind=&quot;$attrs&quot; v-on=&quot;$listeners&quot;&gt;&lt;/child2&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import child2 from &#39;./child2.vue&#39;;
export default {
  name: &#39;demo1&#39;,
  data() {
    return {};
  },
  props: {
    aa: Number
  },
  components: {
    child2
  },
  methods: {
    reduce1() {
      this.$emit(&#39;reduce&#39;);
    }
  }
};
&lt;/script&gt;
// 子组件2
&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;center&quot;&gt;
      &lt;p&gt;bb:{{bb}}&lt;/p&gt;
      &lt;p&gt;child2的$attrs:{{$attrs}}&lt;/p&gt;
      &lt;button @click=&quot;this.reduce2&quot;&gt;组件2减dd&lt;/button&gt;
    &lt;/div&gt;
    &lt;child3 v-bind=&quot;$attrs&quot;&gt;&lt;/child3&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import child3 from &#39;./child3.vue&#39;;
export default {
  name: &#39;demo1&#39;,
  data() {
    return {};
  },
  props: {
    bb: Number
  },
  components: {
    child3
  },
  methods: {
    reduce2() {
      this.$emit(&#39;reduce&#39;);
    }
  }
};
&lt;/script&gt;
// 子组件3
&lt;template&gt;
  &lt;div class=&quot;center&quot;&gt;
    &lt;p&gt;child3的$attrs:{{$attrs}}&lt;/p&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo3&#39;,
  data() {
    return {};
  },
  props: {
    dd: String
  },
};
&lt;/script&gt;

简单来说,$attrs 里存放的是父组件中绑定的非 props 属性,​$listeners 里面存放的是父组件中绑定的非原生事件。

小总结:当传输数据、方法较多时,无需一一填写的小技巧。

五、 Provider / Inject

Vue2.2 版本以后新增了这两个 API, 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。 简单来说,就是父组件通过 Provider 传入变量,任意子孙组件通过 Inject 来拿到变量。

// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;button @click=&quot;this.changeName&quot;&gt;我要改名字了&lt;/button&gt;
    &lt;p&gt;我的名字:{{name}}&lt;/p&gt;
    &lt;child1&gt;&lt;/child1&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Child1 from &#39;./component/child1.vue&#39;;
export default {
  name: &#39;demo&#39;,
  data: function() {
    return {
      name: &#39;政采云&#39;
    };
  },
  // provide() {
  //   return {
  //     name: this.name //这种绑定方式是不可响应的
  //   };
  // },
  provide() {
    return {
      obj: this
    };
  },
  components: {
    Child1
  },
  methods: {
    changeName() {
      this.name = &#39;政采云前端&#39;;
    }
  }
};
&lt;/script&gt;
// 子组件
&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;center&quot;&gt;
      &lt;!-- &lt;p&gt;子组件名字:{{name}}&lt;/p&gt; --&gt;
      &lt;p&gt;子组件名字:{{this.obj.name}}&lt;/p&gt;
    &lt;/div&gt;
    &lt;child2&gt;&lt;/child2&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import child2 from &#39;./child2.vue&#39;;

export default {
  name: &#39;demo1&#39;,
  data() {
    return {};
  },
  props: {},
  // inject: [&quot;name&quot;],
  inject: {
    obj: {
      default: () =&gt; {
        return {};
      }
    }
  },
  components: {
    child2
  },
};
&lt;/script&gt;

需要注意的是:Provide 和 Inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

所以,如果采用的是我代码中注释的方式,父级的 name 如果改变了,子组件this.name 是不会改变的,仍然是 政采云,而当采用代码中传入一个监听对象,修改对象中属性值,是可以监听到修改的。

Provider / Inject 在项目中需要有较多公共传参时使用还是颇为方便的。

小总结:传输数据父级一次注入,子孙组件一起共享的方式。

六、 $parent / $children &amp; $refs

  • $parent / $children:指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.$parent 访问父实例,子实例被推入父实例的 $children 数组中。
  • $refs:一个对象,持有注册过 ref 特性open in new window 的所有 DOM 元素和组件实例。ref 被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs 对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件。
// 父组件
&lt;template&gt;
  &lt;div class=&quot;container&quot;&gt;
    &lt;p&gt;我的title:{{title}}&lt;/p&gt;
    &lt;p&gt;我的name:{{name}}&lt;/p&gt;
    &lt;child1 ref=&quot;comp1&quot;&gt;&lt;/child1&gt;
    &lt;child2 ref=&quot;comp2&quot;&gt;&lt;/child2&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
import Child1 from &#39;./component/child1.vue&#39;;
import Child2 from &#39;./component/child2.vue&#39;;
export default {
  name: &#39;demo&#39;,
  data: function() {
    return {
      title: null,
      name: null,
      content: &#39;就是我&#39;
    };
  },
  components: {
    Child1,
    Child2
  },
  mounted() {
    const comp1 = this.$refs.comp1;
    this.title = comp1.title;
    comp1.sayHello();
    this.name = this.$children[1].title;
  },
};
&lt;/script&gt;
// 子组件1-ref方式
&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;center&quot;&gt;我的父组件是谁:{{content}}&lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo1&#39;,
  data() {
    return {
      title: &#39;我是子组件&#39;,
      content: null
    };
  },
  mounted() {
    this.content = this.$parent.content;
  },
  methods: {
    sayHello() {
      window.alert(&#39;Hello&#39;);
    }
  }
};
&lt;/script&gt;
// 子组件2-children方式
&lt;template&gt;
  &lt;div&gt;
    &lt;div class=&quot;center&quot;&gt;&lt;/div&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script&gt;
export default {
  name: &#39;demo1&#39;,
  data() {
    return {
      title: &#39;我是子组件2&#39;
    };
  },
};
&lt;/script&gt;

通过例子可以看到这两种方式都可以父子间通信,而缺点也很统一,就是都不能跨级以及兄弟间通信。

小总结:父子组件间共享数据以及方法的便捷实践之一。

总结

组件间不同的使用场景可以分为 3 类,对应的通信方式如下:

  • 父子通信:Props / $emit,$emit / $on,Vuex,$attrs / ​$listeners,provide/inject,$parent / $children&$refs
  • 兄弟通信:$emit / $on,Vuex
  • 隔代(跨级)通信:$emit / ​$on,Vuex,provide / inject,$attrs / $listeners

大家可以根据自己的使用场景选择不同的通信方式,当然还是都自己写写代码,试验一把来的印象深刻喽。

招贤纳士(Recruitment)

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一起浪 [坏笑] 如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5年工作时间3年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手参与一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

推荐阅读(Recommend)

看完这篇,你也能把 React Hooks 玩出花open in new window

自动化 Web 性能优化分析方案open in new window

CSS 层叠上下文(Stacking Context)open in new window