本文主要基于的vue 2.x版本

vue中的通信方式比较多,以我个人使用到的做了一些总结,大致有一下几种:

  • props
  • events
  • slot
  • ref
  • eventBus
  • vuex
  • provide / inject

vue通信方式思维图
下面就介绍每种通信方式的使用方式以及各自的区别。

props

props是最常用,最基本的一种通信方式,通过子组件声明参数,父组件直接传入参数。这种方式大家应该都早就烂熟于心了,就没什么好介绍的了,不过需要注意的是,如果子组件没有在props中声明的参数,父组件传入,会被识别为普通的attr属性,子组件将无法通过this直接获取。
一下就是几个要点:

  • 需要提前在组件props中声明
  • String类型的传参,如果不是变量可以直接传入,无需使用:标注,比如<button size='mini'>按钮</button>
  • Boolean类型的传参,如果值是true,且无需变量控制,则可以直接只写参数名,比如<button disabled>按钮</button>

events

events也是最常用的一种通信方式,主要用于子组件返回参数给父组件。最简单的用法就是有子组件触发事件,父组件监听事件。

  1. //子组件
  2. this.$emit('submit',data)
  3. //父组件
  4. <child @submit='submit'/>

@其实就是v-on的缩写,v-on除了可以监听子组件的事件,也可以监听dom的原生事件,并且具有比较多的修饰符来实现更多的方式,官方示例

这边有一个知识点:this.$emit触发的事件,除了父组件可以监听之外,子组件自身也可以监听到该事件,这个vue的文档中有说明,官方示例,具体有什么用途我们后续说到。

props和events是最常用的两种组件通信方式,除了一般的使用方式,vue还提供了inheritAttrs:false属性,用于组件的封装使用,可以让父组件传入的所有属性都通过this.$attrs访问到,这样我们在包装一个组件时,可以通过v-bing=$attrs一次把所有的父组件传入属性绑定到子组件中,v-on=$listeners绑定所有父组件监听事件。

slot

slot插槽也是一种非常常用的组件传参方式,通过slot可以更灵活的控制通用组件的自定义渲染内容,slot除了可以传入vnode渲染内容,也可以通过slot传递参数。

  1. <!-- 接收 prop 的具名插槽 -->
  2. <infinite-scroll>
  3. <template v-slot:item="slotProps">
  4. <div class="item">
  5. {{ slotProps.item.text }}
  6. </div>
  7. </template>
  8. </infinite-scroll>

这种用法在element ui中很多组件都有使用到,比如autocomplete组件的自定义渲染等。

ref

ref实例的方式实现的数据交互是比较强大的,通过调用组件的实例,可以获取到组件所有的数据,实例方法等。通过在子组件上声明ref属性,就可以通过$refs调取到对应的组件实例。
通过调取实例,我们可以操作实例中的所有参数,比如

  1. //代码仅作实例,实际并非如此
  2. <input ref='input' />
  3. this.$refs.input.value = 1

类似以上例子,我们可以在其他组件中直接修改某个组件中的数据,同样也可以读取其数据,但是一般不推荐这种用法。组件的数据应该由组件自身去维护,如果有类似需要修改其数据的情况,我们应该在该组件中声明一个修改数据的方法,而其他组件去调取该方法来修改数据,这样做的目的是让组件更加的可控,不会出现一些莫名其妙的错误,而无法定位。

除了通过ref的声明方式获取组件的实例,我们也可以通过$parent获取父组件的实例,$root获取当前组件树的根实例,$children获取当前组件的子组件实例数组等。
需要注意的是,所有的组件实例都需要在该组件mounted生命周期后才可以被调取到,在该声明周期之前,实例都未挂载,无法正常调取到,这个在vue的文档中有提到:

mounted

实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。

provide / inject

该用法是在vue 2.2之后新增的,主要作用就是用于父组件共享数据给子组件使用,听上去用法是不是和props一样吗?可以说效果确实如此,但是使用的场景不同。
props传参必须是子组件直接使用在父组件中才可以方便传参,如果是子组件是父组件中的子组件的子组件,那么父组件怎么怎么传参给该子组件呢?

比较笨一点的方法,就是父组件先传参给第一层子组件,子组件在传参给其下层子组件。可以说效果是可以达到,但是非常的麻烦,而且如果层次多了,工作量大增的同时,每层组件都得声明一边props,一次传入,非常的不可控,也不优雅。

provide/inject就是为了解决这样的问题。父组件声明需要传递的参数,子组件声明需要接受的参数,中间不管嵌套几层组件,子组件都可以直接在其this中访问到父组件传递的参数。
官方示例:

  1. // 父级组件提供 'foo'
  2. var Provider = {
  3. provide: {
  4. foo: 'bar'
  5. },
  6. // ...
  7. }
  8. // 子组件注入 'foo'
  9. var Child = {
  10. inject: ['foo'],
  11. created () {
  12. console.log(this.foo) // => "bar"
  13. }
  14. // ...
  15. }

这个我们在使用element ui中的form组件应该有感受,我们只需要在form组件上传入label-width,rules等参数,而所有的子组件el-form-item都可以起作用。后面我会以el-form为例,分析一下各种通信方式在其中的应用。

eventBus

从名字就可以看出是一种事件通信方式,其实原理很简单,就是利用的上面提到的组件实例可以监听到自身emit触发的事件,来实现的一种全局通信方式,同时也利用了ref的实例通信方式。使用方式如下:

  1. //main.js
  2. Vue.prototype.$EventBus=new Vue() //在vue的实例上挂载一个vue的实例
  3. //组件A 利用共享的实例注册其监听事件
  4. this.$EventBus.$on('input',(value)=>{console.log(value)})
  5. //组件B 利用共享的实例触发事件
  6. this.$EventBus.$emit('input','test')

这样本质其实是new的vue实例触发事件,监听自己的事件,然后通过共享该实例,来让不同的组件通过该实例来做到数据通信。
需要注意的是,由于该实例是挂载在整个vue实例上的,所以即便在组件销毁之后,事件监听任然是存在的,为了避免重复触发事件,在不需要时或者组件销毁时,通过this.$EventBus.$off去注销监听事件。

vuex

vuex就不多说了,vue中重量级的状态管理管理库了,通过vue的数据的响应式来驱动,具体的用法就直接看文档吧

示例分析

以上差不多把我比较熟知的几种vue通信方式都简要的梳理了一边,但是观看用法可能比较枯燥,不够形象,下面就以element中的form组件为例,来看看该组件的封装中使用了哪些通信方式,element作为vue中最流行的开源框架,很多的设计和实现是非常值得学习的。

直接看源码吧:

  1. //el-form
  2. <template>
  3. ...
  4. </template>
  5. <script>
  6. import objectAssign from 'element-ui/src/utils/merge';
  7. export default {
  8. name: 'ElForm',
  9. componentName: 'ElForm',
  10. provide() {
  11. return {
  12. elForm: this
  13. };
  14. },

这边一上来就可以看到el-form这个父组件使用了provide声明了elForm属性,并传入了自身的实例this,那么很显然,子组件肯定会接受该参数,我们看到子组件代码。

  1. export default {
  2. name: 'ElFormItem',
  3. componentName: 'ElFormItem',
  4. mixins: [emitter],
  5. provide() {
  6. return {
  7. elFormItem: this
  8. };
  9. },
  10. inject: ['elForm'],

果然,ElFormItem中利用inject获取了elForm参数,同时声明了一个elFormItem的共享参数,这个自然也是给其子组件使用的,我们暂时不看。我们先看看通过elForm,ElFormItem组件可以实现什么。

  1. //el-form-item
  2. contentStyle() {
  3. const ret = {};
  4. const label = this.label;
  5. if (this.form.labelPosition === 'top' || this.form.inline) return ret;
  6. if (!label && !this.labelWidth && this.isNested) return ret;
  7. const labelWidth = this.labelWidth || this.form.labelWidth;
  8. if (labelWidth === 'auto') {
  9. if (this.labelWidth === 'auto') {
  10. ret.marginLeft = this.computedLabelWidth;
  11. } else if (this.form.labelWidth === 'auto') {
  12. ret.marginLeft = this.elForm.autoLabelWidth;
  13. }
  14. } else {
  15. ret.marginLeft = labelWidth;
  16. }
  17. return ret;
  18. },
  19. form() {
  20. let parent = this.$parent;
  21. let parentName = parent.$options.componentName;
  22. while (parentName !== 'ElForm') {
  23. if (parentName === 'ElFormItem') {
  24. this.isNested = true;
  25. }
  26. parent = parent.$parent;
  27. parentName = parent.$options.componentName;
  28. }
  29. return parent;
  30. },
  31. _formSize() {
  32. return this.elForm.size;
  33. },

通过上面的代码,我们可以看到子组件直接可以通过this.elForm获取到父组件上传入的一些公共的配置,但是同时我们注意到还有一个formcomputed参数,实现的方式是循环查找到ElForm这个父级组件实例。
这么看来this.elForm岂不是和this.form是一个效果,那这么做的意义是什么呢?
其实这是一个历史原因,因为上面提到provide的特性是在vue 2.2之后才新增的,而element ui早在这之前就有这些组件了,最初的实现方式则是通过computed中的循环查找的方式来获取的父级实例实现的参数共享,而在element ui 2.0之后的版本则新增了provide的方式来共享参数,之后的功能则是基于该特性来实现的,老原来的代码并未使用新特性重构(以上是个人参考github仓库推测而来)

接着往下看,我们知道el-form主要实现的一个功能就是数据校验,我们在输入组件中输入值可以触发el-form的数据校验和错误提示,使用方式就是在el-form上传入mode,rules以及在el-form-item上传入prop,然后就可以根据校验规则触发数据校验,这个是怎么实现的呢,细看代码。

  1. //首先我们找一个简单的组件入手,直接从el-input来看,其他原理都是一样的。
  2. //el-input
  3. inject: {
  4. elForm: {
  5. default: ''
  6. },
  7. elFormItem: {
  8. default: ''
  9. }
  10. }
  11. //computed
  12. _elFormItemSize() {
  13. return (this.elFormItem || {}).elFormItemSize;
  14. },
  15. validateState() {
  16. return this.elFormItem ? this.elFormItem.validateState : '';
  17. },
  18. needStatusIcon() {
  19. return this.elForm ? this.elForm.statusIcon : false;
  20. }
  21. 上面的参数我们可以看到有大量的共享参数的获取,而这些在1.4.13版本的elementui中式没有的,基本也可以验证上面的推测
  22. //methods
  23. handleBlur(event) {
  24. this.focused = false;
  25. this.$emit('blur', event);
  26. if (this.validateEvent) {
  27. this.dispatch('ElFormItem', 'el.form.blur', [this.value]);
  28. }
  29. },

在el-input的失焦事件中,我们可以看到一个this.dispatch('ElFormItem', 'el.form.blur', [this.value]);很明显,这个是和el-from-item组件相关的事件。
不着急,我们先看一下这个dispatch是一个什么方法

  1. //element-ui/src/mixins/emitter.js
  2. function broadcast(componentName, eventName, params) {
  3. this.$children.forEach(child => {
  4. var name = child.$options.componentName;
  5. if (name === componentName) {
  6. child.$emit.apply(child, [eventName].concat(params));
  7. } else {
  8. broadcast.apply(child, [componentName, eventName].concat([params]));
  9. }
  10. });
  11. }
  12. export default {
  13. methods: {
  14. dispatch(componentName, eventName, params) {
  15. var parent = this.$parent || this.$root;
  16. var name = parent.$options.componentName;
  17. while (parent && (!name || name !== componentName)) {
  18. parent = parent.$parent;
  19. if (parent) {
  20. name = parent.$options.componentName;
  21. }
  22. }
  23. if (parent) {
  24. parent.$emit.apply(parent, [eventName].concat(params));
  25. }
  26. },
  27. broadcast(componentName, eventName, params) {
  28. broadcast.call(this, componentName, eventName, params);
  29. }
  30. }
  31. };

可以看到这边有两个方法,一个dispath一个broadcast,从名字就可以看出来,一个是广播事件,一个是派发事件,从dispatch实现上可以看出和上面提到的compued中的form是非常像的,通过向上循环查找对应的组件实例,来触发事件。而boradcast则是怎么向下查找来触发事件,所以他们一个是子组件使用的,一个是父组件使用的,。
所以我们在上面就看到input组件通过该方法向el-form-item来触发输入事件,而el-form-item中我们可以看到利用broadcast事件来重置输入组件的参数等。

说到这边有人就要问了,前面不是说可以通过eventBus的方式实现全局的事件派发和监听吗,为什么要搞这一套,这么麻烦。那是因为element ui作为一个组件库,他是独立使用的,而eventBus则依赖于在main.js中挂载实例才可以使用,作为一个组件库肯定是做到这样的,他必须要可以独立使用,而不能依赖外部不确定性的参数。

  1. resetField() {
  2. this.validateState = '';
  3. this.validateMessage = '';
  4. let model = this.form.model;
  5. ...
  6. this.broadcast('ElTimeSelect', 'fieldReset', this.initialValue);
  7. },

回到el-form-item组件

  1. //el-form-item
  2. addValidateEvents() {
  3. const rules = this.getRules();
  4. if (rules.length || this.required !== undefined) {
  5. this.$on('el.form.blur', this.onFieldBlur);
  6. this.$on('el.form.change', this.onFieldChange);
  7. }
  8. },
  9. removeValidateEvents() {
  10. this.$off();
  11. }
  12. ...
  13. onFieldBlur() {
  14. this.validate('blur');
  15. },
  16. onFieldChange() {
  17. if (this.validateDisabled) {
  18. this.validateDisabled = false;
  19. return;
  20. }
  21. this.validate('change');
  22. },

可以看到确实有监听相关的事件,那么很明显了,input在输入之后触发失焦事件,el-form-item监听输入组件的失焦和change事件,在触发参数校验,而rules则通过父级实例获取到,
同时我们看到onFieldBlur等事件虽然有监听输入事件,但是并没有接收事件触发之后的传值,继续看校验方法:

  1. computed:{
  2. fieldValue() {
  3. const model = this.form.model;
  4. if (!model || !this.prop) { return; }
  5. let path = this.prop;
  6. if (path.indexOf(':') !== -1) {
  7. path = path.replace(/:/, '.');
  8. }
  9. return getPropByPath(model, path, true).v;
  10. },
  11. },
  12. methods: {
  13. validate(trigger, callback = noop) {
  14. this.validateDisabled = false;
  15. const rules = this.getFilteredRule(trigger);
  16. if ((!rules || rules.length === 0) && this.required === undefined) {
  17. callback();
  18. return true;
  19. }
  20. this.validateState = 'validating';
  21. const descriptor = {};
  22. if (rules && rules.length > 0) {
  23. rules.forEach(rule => {
  24. delete rule.trigger;
  25. });
  26. }
  27. descriptor[this.prop] = rules;
  28. const validator = new AsyncValidator(descriptor);
  29. const model = {};
  30. model[this.prop] = this.fieldValue;
  31. validator.validate(model, { firstFields: true }, (errors, invalidFields) => {
  32. this.validateState = !errors ? 'success' : 'error';
  33. this.validateMessage = errors ? errors[0].message : '';
  34. callback(this.validateMessage, invalidFields);
  35. this.elForm && this.elForm.$emit('validate', this.prop, !errors, this.validateMessage || null);
  36. });
  37. },

通过fieldValue的compued就可以看到在el-form上传入的mode的作用的,输入组件的值,是通过父级传入的formData来获取的。通过rules传入的参数校验之后,控制el-form-item的错误消息是否显示,通知利用共享的elForm实例触发校验事件,因此我们可以在elform组件上才可以监听任意输入组件触发的校验事件:
image.png

而所有的输入组件自然是通过slot的方式传入el-fom-item的咯

  1. <div class="el-form-item__content" :style="contentStyle">
  2. <slot></slot>
  3. <transition name="el-zoom-in-top">
  4. <slot
  5. v-if="validateState === 'error' && showMessage && form.showMessage"
  6. name="error"
  7. :error="validateMessage">
  8. <div
  9. class="el-form-item__error"
  10. :class="{
  11. 'el-form-item__error--inline': typeof inlineMessage === 'boolean'
  12. ? inlineMessage
  13. : (elForm && elForm.inlineMessage || false)
  14. }"
  15. >
  16. {{validateMessage}}
  17. </div>
  18. </slot>
  19. </transition>

同时也可以看到有一个name=”error”的slot,传入了一个error的参数,这个就是element ui文中的自定义检验显示方式啦,通过slot传入了错误信息
image.png

可以看到el-form除了没有使用vuex来通信以外,上面的通信方式几乎全部使用到了,所以从优秀的开源项目中,我们是可以学习到非常多优秀的设计和实现的,对于我们的水平提高和开发工作还是很有帮助的。

以上是个人的一些总结和分析,或许有很多说的不正确的地方,和大家交流一下想法,也希望能帮到有需要的童鞋下😁