vue组件间通讯总结

706 阅读8分钟

前言

组件式开发作为 vue 框架核心思想之一,在我们利用 vue 框架做前端开发时,避免不了的要与之打交道。在实际开发中,vue 组件实例间的作用域是相互独立的,但是组件间却是需要相互通信的,共享状态等。所以掌握 vue 组件间何如通信,就显得十分重要,以下总结一下 vue 的 N 种通信,以便回顾和巩固一下知识。

组件间的关系

说到组件间的通信,就有必要了解一下组件间存在哪些关系。 (网上盗图)

组件间常见的关系可以概括分为:

  • 父子:A组件 -> B组件
  • 非父子: 隔代 A组件 -> C组件;兄弟 B组件 -> C组件等

组件间通信的方式

介绍完组件间的关系,可以先来看下 vue 中有哪些通信方式

  1. v-bind 和 props (通过绑定属性进行传值)
  2. v-on 和 $emit (通过触发事件进行传值)
  3. $ref、$parent、$children(通过获取到dom进行传值)
  4. provide和inject (使用依赖注入进行传值)
  5. $attrs 和 $listeners (获取剩余参数进行传值)
  6. EventBus (利用事件总线进行传值)
  7. vuex (利用 vuex 插件进行传值)
  8. 利用本地存储和vue-router等方式

组件间通信方式详解

父子组件间的通信

以下实例均使用vue-cli3 创建的vue工程进行测试
// App.vue 代码如下,后续不在贴出
<template>
 <div id="app">
   <parent></parent>
 </div>
</template>

<script>
import Parent from './components/parent';

export default {
 name: 'App',
 components: {
   Parent
 }
};
</script>

父组件向子组件传值(v-bind 和 props)

// parent 父组件
<template>
  <div>
    <h1>父组件</h1>
    // 在引入的子组件上,通过v-bind绑定一个属性Pmsg,值为msg
    <children :Pmsg="msg"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '来自父组件的消息'
    };
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2>子节点</h2>
    <p>{{ Pmsg }}</p>
  </div>
</template>

<script>
export default {
  name: 'children',
  // 通过props接收在父组件中绑定的属性,然后就可以直接在内部中使用了。
  // 顺带提一句 props可以以数组的方式接收属性,也可以以对象的方式接收
    props: ['Pmsg'],
  // props: {
  //   Pmsg: {
  //     type: String  // 接收的类型,
  //     default:any,
  //     required:Boolean,
  //     validator:Function,
  //   }
  // },
  mounted() {
    console.log(this.Pmsg);
  }
};
</script>

在页面中查看,发现子组件已经接收到父组件传入的值


子组件向父组件传值(v-on 和 $emit)

// parent 父组件
<template>
  <div>
    <h1>父组件</h1>
    <p>{{ msg }}</p>
    // 在子组件标签中添加事件监听,等待子组件触发
    // 注意!监听的是子组件的\$emit('receiveData'),在父组件中触发的是parnetReceiveData
    <children @receiveData="parnetReceiveData"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '父组件原来的值'
    };
  },
  methods: {
    parnetReceiveData(data) {
      console.log(data);
      this.msg = data;
    }
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2>子节点</h2>
    <p @click="emitData">{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'children',
  data() {
    return {
      msg: '子组件的值'
    };
  },
  methods: {
    emitData() {
        // 触发在父组件中,子组件标签上监听的receiveData事件,
        // 从而触发父组件事件,利用参数传递,达到传递值得效果。
      this.$emit('receiveData', this.msg);
    }
  }
};
</script>

在页面中查看效果,启动页面,点击后触发$emit事件,父组件成功接收到子组件传来的值


$parnet 和 $children、$ref

// parent 父组件
<template>
  <div>
    <h1>父组件</h1>
    <p @click="emitData">{{ msg }}</p>
    <children></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '父组件原来的值'
    };
  },
  methods: {
    emitData() {
        // 直接获取到vdom,然后修改值
      console.log('praent的子组件', this.$children);
      this.$children[0].msg = '父组件通过dom方式传过来的值';
    }
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2>子节点</h2>
    <p @click="emitData">{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'children',
  data() {
    return {
      msg: '子组件的值'
    };
  },
  methods: {
    emitData() {
        // 直接获取到vdom,然后修改值
      console.log('children的父组件', this.$parent);
      this.$parent.msg = '子组件通过dom方式传过来的值';
    }
  }
};
</script>

在页面中验证效果,点击父组件,可以看到子组件的值被修改了,同时注意看 $children ,为一个数组需要注意的是 $children 数组的顺序不一定是对的,取决于子组件什么时候插入到父组件的,这点需要特别注意。

点击子组件,同样的可以看到父组件的值已经被修改了,$parent只有一个,所以使用的时候就比较简单

利用ref

// parent 父组件
<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changeData">{{ msg }}</p>
    <children ref="children"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '父组件原来的值'
    };
  },
  methods: {
    changeData() {
        // 答应$refs 和 通过ref获取到vnode,直接修改值
      console.log(this.$refs);
      this.$refs['children'].msg = '通过ref修改的值';
    }
  }
};
</script>
// children 子组件
<template>
  <div class="children">
  // 在子组件中使用ref标识 h2 标签
    <h2 ref="h2">子节点</h2>
    <p @click="showRoot">{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'children',
  data() {
    return {
      msg: '子组件原来的值'
    };
  },
  methods: {
    showRoot() {
        // 打印\$root 
      console.log(this.$root);
    }
  }
};
</script>

页面中点击父组件,控制台可以看到父组件的refs对象,分别保存了父组件中通过ref标识的两个vnode,通过向下查找可以看到在子组件的vnode中同样存在子组件中标识的ref

顺带看一下$root,其实就是vue实例

小拓展

介绍完前面三种,父子组件间传值就基本介绍完毕。 通过了解了 props 和 $emit 两种方式,就可以顺带扩展一下 v-model 这个api 和 .sync 修饰符

  1. v-model 是一个语法糖,其实质是通过 props 和 $emit 来达到数据双向绑定的效果的 在input中
// parnet 父组件
<template>
  <div>
    <h1>父组件</h1>
    <p>{{ msg }}</p>
    // 利用v-model达到数据双向绑定的效果
    <input type="text" v-model="msg">
  </div>
</template>

<script>
export default {
  name: 'praent',
  data() {
    return {
      msg: '父组件原来的值'
    };
  },
};
</script>

在页面中的input框中输入或者修改值,p标签也会同时被修改

现在将 v-model 改为用 props 和 $emit 的方式去实现

// parent 父组件
<template>
  <div>
    <h1>父组件</h1>
    <p>{{ msg }}</p>
    <input
      type="text"
      :value="msg"
      // 监听input,获取事件的值的同时赋值给msg
      @input="msg = $event.target.value"
    >
  </div>
</template>

<script>
export default {
  name: 'praent',
  data() {
    return {
      msg: '父组件原来的值'
    };
  },
};
</script>

是可以得到同样的效果,有兴趣的可以是试一下

但是,在 vue 中, v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:

  • text 和 textarea 元素使用 value property 和 input 事件;
  • checkbox 和 radio 使用 checked property 和 change 事件;
  • select 字段将 value 作为 prop 并将 change 作为事件。

这些均可以在 vue 的的官方帮助文档上找到,以及自定义组件的 v-model 自定义事件的使用详情

  1. .sync 修饰符 先看 .sync修饰符的使用方式
// parent 父组件
<template>
  <div>
    <h1>父组件</h1>
    <p>{{ msg }}</p>
    // 在传递的数据中,添加.sync修饰符
    <children :msg.sync="msg"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '父组件原来的值'
    };
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2>子节点</h2>
    <p @click="syncData">{{ msg }}</p>
  </div>
</template>

<script>
export default {
  name: 'children',
  props: ['msg'],
  methods: {
    syncData() {
        // 这里需要 \$emit 触发update:msg 事件,并且把要修改的值进行传递
      this.$emit('update:msg', '通过sync方式修改msg值');
    }
  }
};
</script>

启动页面,在页面中点击子组件的值

// .sync 修饰符其实就是将以下形式做成了一个语法糖
<template>
  <div>
    <h1>父组件</h1>
    <p>{{ msg }}</p>
    // 就是对@update:msg 改为.sync
    <children
      :msg="msg"
      @update:msg="msg = $event"
    ></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '父组件原来的值'
    };
  }
};
</script>

非父子组件传值

provide和inject

provide 和 inject 适用于隔代传值,爷孙或者更深的层级。

官方说明:这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

同时需要注意官网中的该条提示:

提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。

也就是说,Vue 并不会对 provide 中的变量进行响应式处理。但是如果我传递的变量已经是经过响应式处理的 inject 接受的变量也会是响应式的。 以下就通过代码来说明 先介绍以下两个api的使用

// provide
// 1. 可以使用工厂函数返回一个对象,使用该方法就可以获取到this
provide: function () {
  return {
    msg: this.msg
  }
}

// 2. 可以直接为一个对象
provide: {
  msg: '传入的值'
}
// inject 
// 1. 接收一个数组
inject: ['msg']

// 2. 接收一个对象
inject: {
    msg: { // 当然msg的值也可以直接直接为字符串
        from: 'msg' // 注入内容中的 key
        default: '' // 默认值,也可以是个工厂函数
    }
}

非响应式的数据传值代码演示

// parent 父组件
<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changeData">{{ msg }}</p>
    <children ref="children"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '来自父亲的值'
    };
  },
  methods: {
    changeData() {
      this.msg = '父组件改变了向下传递的msg值';
    }
  },
  provide() {
    return {
      msg: this.msg
    };
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ msg }}</p>
    <grandson></grandson>
  </div>
</template>

<script>
import Grandson from './grandson';
export default {
  name: 'children',
  components: {
    Grandson
  },
  inject: ['msg']
};
</script>

启动页面,点击父组件p标签,父组件的值已经修改了,向下传递的值并没有修改

如果传入的值为响应式的

<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changeData">{{ msg.value }}</p>
    <children ref="children"></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
        // 稍微把msg的值改为一个对象
      msg: { value: '来自父亲的值' }
    };
  },
  methods: {
    changeData() {
      this.msg.value = '父组件改变了向下传递的msg值';
    }
  },
  provide() {
    return {
      msg: this.msg
    };
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ msg.value }}</p>
    <grandson></grandson>
  </div>
</template>

<script>
import Grandson from './grandson';
export default {
  name: 'children',
  components: {
    Grandson
  },
  inject: ['msg']
};
</script>
// grandson 孙组件
<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p>{{ msg.value }}</p>
  </div>
</template>

<script>
export default {
  name: 'grandson'
  inject: ['msg']
};
</script>

启动页面,此时再去点击父组件的p标签,现在不单单父组件的值更新了,连同子组件和孙组件的值都发生变化了

将孙组件稍微修改一下,定义一个值去存储inject接受到的响应式的值,再通过点击事件去修改值,

<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p @click="changeMsg">{{ grandsonMsg.value }}</p>
  </div>
</template>

<script>
export default {
  name: 'grandson',
  data() {
    return {
      grandsonMsg: ''
    }
  },
  mounted(){
this.grandsonMsg = this.msg
  },
  inject: ['msg'],
  methods:{
    changeMsg(){
      this.grandsonMsg.value = '孙组件修改了msg值'
    }
  }
};
</script>

可以看到,孙组件接受到这个响应式的值,然后通过这种的方式去修改这个值,同样可以修改到父组件provide下来的值

provide 和 inject 大量存在于高阶组件中,如element-ui中form表单,button按钮等,均使用了这两个api

使用该api,有利于解耦组件间的关系,使得组件的层级就不在有强关联了。同时也需要慎重使用,因为其修改值是没办法进行追踪的,同时破坏了单向数据流原则。

$attrs 和 $listeners

官方文档中对$attrs的说明

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

也就是说所接受到的属性是不包含class、style、和已经被prop获取的到的,等下在代码中可以注意一下。直接看代码 同时注意一下子组件中的配置项inheritAttrs。

官方文档对inheritAttrs配置项的说明

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上

// parent 父组件
<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p>{{ msg }}</p>
    <children
      :msgToChildren="msgToChildren"
      :msgToGrandson="msgToGrandson"
    ></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msgToChildren: '父亲的值传给孩子的值',
      msgToGrandson: '父亲的值传给孙子的值'
    };
  }
};
</script>
// 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ this.msgToChildren }}</p>
    <grandson v-bind="$attrs"></grandson>
  </div>
</template>

<script>
import Grandson from './grandson';
export default {
  name: 'children',
  // 
  // inheritAttrs: false,
  components: {
    Grandson
  },
  props: ['msgToChildren']
};
</script>
// grandson 孙组件
<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p>{{ msgToGrandson }}</p>
  </div>
</template>

<script>
export default {
  name: 'grandson',
  props: ['msgToGrandson']
};
</script>

运行页面,孙组件可以拿到父组件传下去的值,同时注意子组件标签上的属性,msgToGrandson依旧存在

将子组件中inheritAttrs配置项这是为false

// children 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ this.msgToChildren }}</p>
    <grandson v-bind="$attrs"></grandson>
  </div>
</template>

<script>
import Grandson from './grandson';
export default {
  name: 'children',
  inheritAttrs: false,
  components: {
    Grandson
  },
  props: ['msgToChildren']
};
</script>

再看到页面,此时子组件上的msgToGrandson的属性就被抛弃了

看完$attrs的使用,再来看$listeners的使用

官方文档对 $listrners的说明

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

注意的是$listeners 可以监听到所有的事件,直接看代码实现

<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changrMsg">{{ msg }}</p>
    <children
      :msgToChildren="msgToChildren"
      :msgToGrandson="msgToGrandson"
      @changeFromChildren="changeFromChildren"
      @changeFromGrandson="changeFromGrandson"
    ></children>
  </div>
</template>

<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '重置父亲的值',
      msgToChildren: '父亲的值传给孩子的值',
      msgToGrandson: '父亲的值传给孙子的值'
    };
  },
  methods: {
    changrMsg() {
      console.log('重置msg');
      this.msgToChildren = '父亲的值传给孩子的值';
      this.msgToGrandson = '父亲的值传给孙子的值';
    },
    changeFromChildren() {
      console.log('触发父组件changeFromChildren事件');

      this.msgToChildren = '子组件改变了msg值';
    },
    changeFromGrandson() {
      console.log('触发父组件changeFromGrandson事件');
      this.msgToGrandson = '孙组件改变了msg值';
    }
  },
  provide() {
    return {
      msg: this.msg
    };
  }
};
</script>
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p @click="changeMsg">{{ this.msgToChildren }}</p>
    <grandson
      v-bind="$attrs"
      v-on="$listeners"
    ></grandson>
  </div>
</template>

<script>
import Grandson from './grandson';
export default {
  name: 'children',
  inheritAttrs: false,
  components: {
    Grandson
  },
  props: ['msgToChildren'],
  methods: {
    changeMsg() {
      this.$emit('changeFromChildren');
    }
  }
};
</script>
<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p @click="changeMsg">{{ msgToGrandson }}</p>
    <p @click="changeChildrenMsg">修改子组件</p>
  </div>
</template>

<script>
export default {
  name: 'grandson',
  props: ['msgToGrandson'],
  methods: {
    changeMsg() {
      this.$emit('changeFromGrandson');
    },
    changeChildrenMsg() {
      this.$emit('changeFromChildren');
    }
  }
};
</script>

小总结

跨层级的组件通信也基本介绍完毕,一般而言$attrs 和 $listeners 使用于爷孙组件之间的通信,如果层级太深,需要再每一层中绑定$attrs 和 $listeners 属性,此时就应该考虑使用其他方式,而provide和inject使用于祖辈向其子孙传递值,再一定场景下,可以利用其来实现兄弟组件的状态共享。 介绍完这几种方式其实vue提供的组件通信方式也基本就在这里了,然后我们可以发现,vue确实是没有提供直接用于兄弟组件间通信的方式。那么兄弟组件间通信我们可以实现呢?

兄弟组件通信

由于vue并没有提供直接通信的方式,一般需要我们使用其他方式进行通信。

EventBus 事件总线

事件总线的原理其实就是使用发布订阅模式,创建一个中介,A组件把需要传递的值或者触发的事件告知中介,由中介去告诉B组件。 先简单实现一个发布订阅

class Eventbus {
  constructor() {
    this.list = {};
  }

  $on(type, fn) {
    if (this.list[type]) {
      this.list[type].push(fn);
    } else {
      this.list[type] = [];
      this.list[type].push(fn);
    }
  }
  $emit(type) {
    if (this.list[type]) {
      this.list[type].forEach(fn => {
        fn();
      });
    }
  }
}

const eventBus = new Eventbus();

export default eventBus;

在父组件、第一组件加载完成时添加一个监听事件

// parent 父组件
<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p>{{msg}}</p>
    <first></first>
    <second></second>
  </div>
</template>

<script>
import First from './first';
import Second from './second';
import eventBus from './eventBus';
export default {
  name: 'praent',
  components: {
    First,
    Second
  },
  data() {
    return {
      msg: '父亲的值'
    };
  },
  mounted() {
    eventBus.$on('aaa', () => {
      console.log('父组件的msg被修改了');
      this.msg = '父组件的msg被修改了';
    });
  }
};
</script>
// first第一组件
<template>
  <div class="first">
    <h3>第一兄弟组件</h3>
    <p>{{ msg }}</p>
  </div>
</template>

<script>
import eventBus from './eventBus';
export default {
  name: 'first',
  data() {
    return {
      msg: '第一兄弟组件'
    };
  },
  mounted() {
    eventBus.$on('aaa', () => {
      console.log('第一兄弟组件的msg被修改了');
      this.msg = '第一兄弟组件的msg被修改了';
    });
  }
};
</script>

在第二组件中添加一个事件,该事件触发时会去触发定义在eventBus中的事件

<template>
  <div class="second">
    <h3>第二兄弟组件</h3>
    <p @click="emitEventBus">第二兄弟组件</p>
  </div>
</template>

<script>
import eventBus from './eventBus';
export default {
  name: 'second',
  methods: {
    emitEventBus() {
      console.log('第二兄弟组件触发事件');
      eventBus.$emit('aaa');
    }
  }
};
</script>

当我们点击第二组件时,可以看到,无论时父子组件,还是兄弟组件,只要是$on 监听了eventBus中的事件,只要在任何地方通过eventBus $emit该事件,就可以执行。

当然在实际开发中,vue 实例内部本身已经实现了事件的监听和触发,我们可以利用vue实例来做属性和事件的监听,并不需要我们自己去实现一个发布订阅

我们可以将eventBus文件修改下

import Vue from 'vue'
const eventBus = new Vue()
export default eventBus;

可以自行测试一下,同样是可以使用的

同时,我们还可以在main.js中将event挂载到vue根实例的prototype上,这样就不用再每次需要使用eventBus 都要去引用

// main.js
import Vue from 'vue'
import App from './App.vue'
import eventBus from './components/eventBus';

Vue.prototypt.$eventBus = eventBus
Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

添加监听和emit事件只需要修改一下代码即可。使用起来就方便很多

    this.$eventBus.$on('aaa', () => {
      console.log('第一兄弟组件的msg被修改了');
      this.msg = '第一兄弟组件的msg被修改了';
    });
    
    this.$eventBus.$emit('aaa');

vuex

剩下最后一个vueX,可以直接阅读官方文档 vuex 查看他的使用,

后续有时间就单独写一篇。