vue 组件间通信

119 阅读3分钟

① 父给子传值 props;② $attrs 给子组件传值;③ $parent/$children;④ provide / inject;⑤ 子组件向父组件传递 emit;⑥ ref/$refs; ⑦ eventBus 事件总线。

父给子传值 props

我们通过在父组件引用子组件的时候,可以通过自定义属性进行值的传递,在子组件通过 props 进行接收。

/**
* 选项式 API写法:
*/
// parent.vue:
<template>
  <div class="wrapper">
    <Child :toChild="testData"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child.vue';

const testData = ref('传递给子组件的值');
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <p>{{ toChild }}</p>
  </div>
</template>

<script>
export default {
  // 写法一:
  props: ['toChild'],
  
  // 写法二:
  props: ['toChild'], // 需要声明一下接受到了,否则会报警告
  setup(props) { // 此处 props 不能省略
    console.log(props); // Proxy {toChild: '传递给子组件的值'}
  },
};
</script>

// parent 组件渲染为:
<div class="wrapper">
   <div class="child_wrapper">
      <p>传递给子组件的值</p>
   </div>
</div>
/** 
* setup 语法糖写法: 
*/
// parent.vue:
<template>
  <div class="wrapper">
    <Child :toChild="testData"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child.vue';

const testData = ref('传递给子组件的值');
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <p>{{ propsData }}</p>
  </div>
</template>

<script setup>
// 写法一:
const propsData = defineProps(['toChild']);

// 写法二:
const propsData = defineProps({
  toChild: String,
});

// 写法三:
const propsData = defineProps({
  toChild: {
    type: String,
    default: '默认值',
    require: false,
  },
});
</script>

// parent 组件渲染为:
<div class="wrapper">
   <div class="child_wrapper">
      <p>{"toChild": "传递给子组件的值"}</p>
   </div>
</div>

注意:① 在启用 eslint 的情况下,调用 defineProps(),会出现 'defineProps' is not defined 报错,需要修改 .eslintrc.js 的配置;② defineProps()返回值为 Proxy 对象。

// .eslintrc.js:
module.exports = {
    ...
    env: {
        node: true,
        // 需要添加这个配置
        'vue/setup-compiler-macros': true,
    },
    ...
}

$attrs 给子组件传值

① 我们的 $attrs 只会包括未被 props 继承的属性;② 当引用的子组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中;③ 如果你不希望组件的根元素继承 attribute,可以在组件的选项中设置 inheritAttrs: false;④ 多个根节点上的 attribute 继承:子组件如果有多个根节点,那么这些根节点不具有自动绑定 attribute 的行为,需要通过 v-bind="$attrs" 在根节点上绑定,没有一个都没有绑定就会报警告多节点不能自动绑定 attribute ,需要手动指定;⑤ 非继承的属性,如果属性名有大写字母,会被转为小写。

// parent.vue:
<template>
  <div class="wrapper">
    <Child :selfData="testData" data-status="activated" @click="handle"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child.vue';

const testData = ref('传递给子组件的值');
const handle = () => {};
</script>

// child.vue:
<template>
  <div class="child_wrapper"></div>
</template>

<script>
export default {
  created() {
    console.log(this.$attrs); // Proxy {selfData: '传递给子组件的值', data-status:  
  },                          // 'activated', __vInternal: 1, onClick: ƒ}
};
</script>

// parent 组件渲染为:
<div class="wrapper">
   <div class="child_wrapper" selfdata="传递给子组件的值" data-status="activated"></div>
</div>
// parent.vue:
<template>
  <div class="wrapper">
    <Child :toChild="testData" data-status="activated" @click="handle"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child-view.vue';

const testData = ref('传递给子组件的值');
const handle = () => {};
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <p>{{ toChild }}</p>
  </div>
</template>

<script>
export default {
  props: ['toChild'],
  created() {
    console.log(this.$attrs); // Proxy {data-status: 'activated', __vInternal: 1,
  },                          // onClick: ƒ}
};
</script>

// parent 组件渲染为:
<div class="wrapper">
   <!--toChild 属性被子组件继承,故不在子组件根节点上-->
   <div class="child_wrapper" data-status="activated"> 
      <p>传递给子组件的值</p>
   </div>
</div>
// parent.vue:
<template>
  <div class="wrapper">
    <Child :toChild="testData" data-Define="define" data-status="activated" 
        @click="handle"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child-view.vue';

const testData = ref('传递给子组件的值');
const handle = () => {};
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <p>{{ toChild }}</p>
  </div>
</template>

<script>
export default {
  inheritAttrs: false, // 禁用根元素继承 attribute
  props: ['toChild'],
  created() {
    console.log(this.$attrs); // Proxy {data-Define: 'define', data-status: 
  },                          // 'activated', __vInternal: 1, onClick: ƒ}
};
</script>

// parent 组件渲染为:
<div class="wrapper">
   <div class="child_wrapper">
      <p>传递给子组件的值</p>
   </div>
</div>
// parent.vue:
<template>
  <div class="wrapper">
    <Child :toChild="testData" data-Define="define" data-status="activated" 
        @click="handle"></Child>
  </div>
</template>

<script setup>
import { ref } from '@vue/reactivity';
import Child from './child.vue';

const testData = ref('传递给子组件的值');
const handle = () => {};
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <p>{{ toChild }}</p>
  </div>
  <!-- 如果 child_2, child_3 都没有 v-bind="$attrs", 就会报警告-->
  <div class="child_2" v-bind="$attrs"></div> 
  <div class="child_3" v-bind="$attrs"></div>
</template>

<script>
export default {
  props: ['toChild'],
  created() {
    console.log(this.$attrs); // Proxy {data-Define: 'define', data-status: 
  },                          // 'activated', __vInternal: 1, onClick: ƒ}
};
</script>

// parent 组件渲染为:
<div class="wrapper">
   <div class="child_wrapper"> <!--toChild 属性被子组件继承,故不在节点上-->
      <p>传递给子组件的值</p>
   </div>
   <div class="child_2" data-define="define" data-status="activated"></div>
   <div class="child_3" data-define="define" data-status="activated"></div>
</div>

$parent/$children

需要注意的是:① 这两种都是直接得到组件实例,使用后可以直接调用组件的方法或访问数据;② vue3 中已经移除 $children,故 vue3 不支持 $children

通过 $parent 获取父级(父实例)的时候,可以获取父级的全部属性。

// parent.vue:
<template>
  <Child></Child>
</template>

<script>
import Child from './child.vue';
export default {
  components: {
    Child,
  },
  setup() {
    const b = 30;
    const c = () => {};
    return { b, c };
  },
  data() {
    return {
      a: 20,
    };
  },
  methods: {
    say() {},
  },
  computed: {
    getInfor() {
      return '获取信息';
    },
  },
};
</script>

// child.vue:
<template>
  <div></div>
</template>

<script>
export default {
  created() {
    console.log(this.$parent.a); // 20
    console.log(this.$parent); // Proxy {a: 20, b: 30, c: ()=>{}, getInfor:
  },                           // "获取信息", say: f()}
};
</script>

通过 $children 获取子级(子实例)的时候,可以获取子级的全部属性。

// parent.vue:
<template>
  <div class="wrapper">
    <Child></Child>
  </div>
</template>

<script>
import Child from './child.vue';

export default {
  components: {
    Child
  },
  data() {
    return {
      obj: {}
    };
  },
  mounted() {
    console.log(this.$children); // 子组件实例列表(即组件实例的集合)
    console.log(this.$children[0].a); // 10
    console.log(this.$children[0].say); // ƒ say() {}
  }
};
</script>

// child.vue:
<template>
  <div class="child_wrapper"></div>
</template>

<script>
export default {
  data() {
    return {
      a: 10
    };
  },
  methods: {
    say() {}
  }
};
</script>

provide / inject

在 vue2 中

provide/inject 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。一言而蔽之:祖先组件中通过 provide 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。

以下写法 vue3 也支持。

// parent.vue:
<template>
  <Child></Child>
</template>

<script>
import Child from './child.vue';

export default {
  components: {
    Child,
  },
  provide: {
    name: '张三',
  },
};
</script>

// child.vue:
<template>
  <button>子组件发送数据</button>
</template>

<script>
export default {
  inject: ['name'],
  created() {
    console.log(this.name); // 张三
  },
};
</script>

按照上边的例子,发现如果修改了 Provide 的 name 值,Child 的 name 并没有随着更改。官网也说了 Provide 并不是响应的。为了解决这个问题 需要把 name 变成 vue 的监听的对象,换句话说就是需要把 name 变成对象。

// parent.vue:
<template>
  <Child></Child>
  <button @click="handleData">修改数据</button>
</template>

<script>
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  data() {
    return {
      obj: {
        name: '张三',
      },
    };
  },
  provide() {
    return {
      obj: this.obj,
    };
  },
  methods: {
    handleData() {
      this.obj.name = '修改了张三';
    },
  },
};
</script>

// child.vue:
<template>
  <p>{{ obj }}</p>
</template>

<script>
export default {
  inject: ['obj'],
  created() {
    console.log(this.obj);
  },
};
</script>

在 vue3 中

在vue2中我们已经使用过provide和inject来实现祖孙组件之间的数据传递,但是在vue3中由于我们使用setup,此时我们应该如何去使用provide和inject函数呢?

在vue中帮我们提供了provide和inject的函数,我们可以直接在setup函数中使用即可。

注意:provide()提供的数据,子组件通过 inject()注入后,子组件可以直接修改父组件的值。但是在一些情况下,我们是不允许的,此时我们可以采用readonly来进行设置。

/**
* setup 语法糖写法:
*/
// parent.vue:
<template>
  <Child></Child>
</template>

<script setup>
import { reactive, ref } from '@vue/reactivity';
import { provide } from '@vue/runtime-core';
import Child from './child.vue';

const a = { x: 10, y: 20 };
const b = 'code';
provide('A1', a);
provide('A2', reactive(a));
provide('B', ref(b));
</script>

// child.vue:
<template>
  <div></div>
</template>

<script setup>
const { inject } = require('@vue/runtime-core');

const A1 = inject('A1');
const A2 = inject('A2');
const B = inject('B');
console.log(A1, A2, B); // {x: 10, y: 20}
</script>               // Proxy {x: 10, y: 20} 
                        // RefImpl {value: "code"}
/**
* 选项式 API 写法:
*/
// parent.vue:
<template>
  <Child></Child>
</template>

<script>
import { reactive, ref } from '@vue/reactivity';
import { provide } from '@vue/runtime-core';
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  setup() {
    const a = { x: 10, y: 20 };
    const b = 'code';
    provide('A1', a);
    provide('A2', reactive(a));
    provide('B', ref(b));
  },
};
</script>

// child.vue:
<template>
  <div></div>
</template>

<script>
const { inject } = require('@vue/runtime-core');
export default {
  setup() {
    const A1 = inject('A1');
    const A2 = inject('A2');
    const B = inject('B');
    console.log(A1, A2, B); // {x: 10, y: 20}
  },                        // Proxy {x: 10, y: 20} 
};                          // RefImpl {value: "code"}
</script>
/**
* 设置 provide() 提供的值只读,子组件无法修改:
*/
// parent.vue:
<template>
  <Child></Child>
</template>

<script>
import { reactive, ref, readonly } from '@vue/reactivity';
import { provide } from '@vue/runtime-core';
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  setup() {
    const a = { x: 10, y: 20 };
    const b = 'code';
    provide('A1', a);
    provide('A2', readonly(reactive(a))); // reactive(a) 的值只读,子组件无法修改
    provide('B', ref(b));
  },
};
</script>

// child.vue:
<template>
  <div>{{ B }}</div>
  <button @click="handleData">修改父组件传递的数据</button>
</template>

<script>
const { inject } = require('@vue/runtime-core');
export default {
  setup() {
    const A1 = inject('A1');
    const A2 = inject('A2');
    const B = inject('B');
    console.log(A1, A2, B);
    const handleData = () => {
      B.value = '修改了 code';
    };
    return { B, handleData };
  },
};
</script>
/**
* 父组件中定义修改父组件数据的方法,并提供给子组件调用:
*/
// parent.vue:
<template>
  <Child></Child>
</template>

<script>
import { reactive, ref, readonly } from '@vue/reactivity';
import { provide } from '@vue/runtime-core';
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  setup() {
    const a = { x: 10, y: 20 };
    let b = ref('code');
    const changeCode = () => {
      b.value = '修改了 code';
    };
    provide('A1', a);
    provide('A2', readonly(reactive(a))); // reactive(a) 的值只读,子组件无法修改
    provide('B', b);
    provide('changeCode', changeCode);
  },
};
</script>

// child.vue:
<template>
  <div>{{ B }}</div>
  <button @click="handleData">直接修改父组件传递的数据</button>
  <button @click="changeCode">调用父组件提供的方法修改父组件的数据</button>
</template>

<script>
const { inject } = require('@vue/runtime-core');
export default {
  setup() {
    const A1 = inject('A1');
    const A2 = inject('A2');
    const B = inject('B');
    console.log(A1, A2, B);
    const handleData = () => {
      // 不建议子组件直接修改父组件中的值
      B.value = '修改了 code';
    };
    const changeCode = inject('changeCode'); // 注入父组件提供修改父组件数据的方法
    return { B, handleData, changeCode };
  },
};
</script>

子组件向父组件传递 emit

/**
* 但是这种方法在 Vue3.x 中不推荐使用。
*/
// parent.vue:
<template>
  <Child @testEmit="testEmit"></Child>
</template>

<script>
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  methods: {
    testEmit(data) {
      console.log('子组件传递的数据:', data); // 子组件传递的数据: child data
    },
  },
};
</script>

// child.vue:
<template>
  <button @click="sendData">子组件发送数据</button>
</template>

<script>
export default {
  methods: {
    sendData() {
      this.$emit('testEmit', 'child data');
    },
  },
};
</script>

vue3 中推荐写法:通过 defineEmits()定义事件,并返回一个函数。

// parent.vue:
<template>
  <Child @testEmit="testEmit"></Child>
</template>

<script>
import Child from './child-view.vue';

export default {
  components: {
    Child,
  },
  methods: {
    testEmit(data) {
      console.log('子组件传递的数据:', data); // 子组件传递的数据: child data
    },
  },
};
</script>

// child.vue:
<template>
  <button @click="sendData">子组件发送数据</button>
</template>

<script setup>
const { defineEmits } = require('@vue/runtime-core');

const emit = defineEmits(['testEmit']);
const sendData = () => {
  emit('testEmit', 'child data');
};
</script>

ref/$refs

作用:获取节点或组件实例。
场景:简单的获取节点或组件实例的属性或者方法,但并不改变其数据。
缺陷:必须在模板渲染之后,不是响应式的,时不时配合$nextTick

ref 放在不同的位置,有不同的效果:

  • 元素节点时,可以通过this.$refs.元素节点 ref 值得到节点的属性或者方法,如<p ref="p">hello</p>
  • 组件时,可以通过this.$refs.组件 ref 值得到相应的组件实例,从而得到组件上面的属性和方法,如<child-component ref="child"></child-component>
  • v-for语法时,可以通过this.$refs.items得到节点或组件实例的数组,具体的某项,需要this.$refs.items[index],如<item ref="items" v-for=".">hello</item>

ref 要在组件渲染完成之后才能生效

ref 是以属性的方式存在标签上,所以在组件渲染完成之后才会生效。 因此$refs 不是响应式的,避免在模板或计算属性中访问 $refs

// ref 在元素节点上:
<template>
  <div class="child_wrapper">
    <div ref="divDom">
      <p>子节点</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return { };
  },
  mounted() {
    console.log(this.$refs.divDom); // <div><p>子节点</p></div>
  }
};
</script>

// ref 在组件上:
// parent.vue:
<template>
  <div class="wrapper">
    <Child ref="childDom" data-active="active"></Child>
  </div>
</template>

<script>
import Child from './child.vue';

export default {
  components: {
    Child
  },
  data() {
    return {
      obj: {}
    };
  },
  mounted() {
    console.log(this.$refs.childDom); // 获取到子组件实例
    console.log(this.$refs.childDom.a); // 10
    console.log(this.$refs.childDom.init); // ƒ init() {}
    console.log(this.$refs.childDom.$attrs); // {data-active: 'active'} (获取到子组件
  }                                          // 根节点的属性)
};
</script>

// child.vue:
<template>
  <div class="child_wrapper">
    <div ref="childDom">
      <p>子节点</p>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      a: 10
    };
  },
  methods: {
    init() {}
  },
  mounted() {
    console.log(this.$attrs); // {data-active: 'active'}
  }
};
</script>
  • created的时候,$refs只是一个空对象。
  • mounted的时候,$refs才有数据,但没有渲染的节点或者组件依旧获取不到
  • 将某个节点或者组件,v-if的值从false=>true的瞬间,因为模板还未更新,所以依旧获取不到
  • 常配合nextTick

eventBus 事件总线

vue3 不支持 eventBus 了,因为原先实例上的三个方法 $on$off$once 被删除掉了。

$on 必须比 $emit 先加载,否则无法监听到传递的数据。

作用:实现任意组件间的通信
有如下两种实现方式
① 全局事件总线:
在 main.js 文件中定义

// src/main.js:
new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App),
    beforeCreate() {
        Vue.prototype.$bus = this;
    }
})

// 或者:
Vue.prototype.$bus = new Vue();

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App),
})

使用方法

// 传值 (可以在你定义的方法中传值)
this.$bus.$emit('定义名称',值);

// 监听数据 (在mounted监听数据)
this.$bus.$on('定义名称' val=>{});

// 销毁(在beforeDestroy销毁)
this.$bus.$off('定义名称');

② 局部事件总线:
在main.js的同级建一个bus.js

// src/bus.js:
import Vue from "vue";
export default new Vue;

使用方法

// 1.引入(传值和监听数据部分都需引入bus.js文件)
import Bus from '@bus.js';

// 传值(我是在beforeDestroy中传值的)
Bus.$emit('定义名称',值);

// 监听数据 (我在beforeCreate 中监听数据)
Bus.$on('定义名称',val=>{});

//销毁(在监听数据页面的 beforeDestroy 中销毁)
Bus.$off('定义名称');

拓展
Vue 事件中央总线
vue 之 中央事件总线 bus