Vue组件间通信的几种方式

今天讲讲 Vue 组件间的几种通信方式。

图片[1]-Vue组件间通信的几种方式-不念博客
Vue组件间通信的几种方式

props

Vue 遵循单向数据流的原则,状态会从父组件传递给子组件,避免子组件意外改变父组件状态导致的混乱逻辑。

父组件通过 props 传数据给子组件。

组合式写法

父组件将 msg 传入到子组件的 text prop,使用 v-bind:props 语法。

<!-- Parent -->
<script setup>
import { ref } from 'vue';
import Child from './child.vue';
const msg = ref('来自父组件的数据');
</script>

<template>
  <Child :text="msg" />
</template>

子组件通过 defineProps 接收:

<!-- Child -->
<script setup>
const props = defineProps({
  text: String // 运行时的类型校验,需要为字符串类型
});
</script>

<template>
  <div>{{ props.text }}</div>
</template>

选项式写法

父组件:

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

export default {
  components: { Child },
  data() {
    return {
      msg: '来自父组件的数据',
    };
  },
};
</script>

<template>
  <Child :text="msg" />
</template>

子组件:

<!-- Child -->
<script>
export default {
  props: {
    text: String
  }
}
</script>

<template>
  <div>{{ text }}</div>
</template>

emit

子组件使用 emit 向父组件通信。

组合式写法

父组件通过 v-on:eventName (缩写为 @eventName)来监听子组件的事件,能够拿到子组件传过来的参数:

<!-- Parent -->
<script setup>
import { ref } from 'vue';
import Child from './child.vue';
const msgFromChild = ref("");
</script>

<template>
  {{ msgFromChild }}
  <Child @update="m => { msgFromChild = m }" />
</template>

子组件:

<!-- Child -->
<script setup>
const emit = defineEmits(['update']);
emit('update', '来自子组件的数据');
</script>

<template>
  <div></div>
</template>

选项式写法

父组件:

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

export default {
  components: { Child },
  data() {
    return {
      msgFromChild: '',
    };
  },
};
</script>

<template>
  {{ msgFromChild }}
  <Child @update="m => { msgFromChild = m }" />
</template>

子组件:

<!-- Child -->
<script>
export default {
  // emits 可省略
  // 如果加上,Vue 会帮你在运行时做校验
  emits: ['update'],
  created() {
    this.$emit('update', '来自子组件的数据 2');
  }
}
</script>

<template>
  <div></div>
</template>

ref

https://cn.vuejs.org/guide/essentials/template-refs.html#ref-on-component

ref 代表一个引用,我们可以通过它来拿到原生 DOM 元素,或组件对象(选项式下)或自定义的对象(组合式下)。

组合式

父组件,创建一个 ref,绑定到子组件的特殊的 ref prop 上:

<!-- Parent -->
<script setup>
import { onMounted, ref } from 'vue';
import Child from './child.vue';
const childRef = ref(null);
onMounted(() => {
  // ref 需要子组件构建好才有值,所以
  console.log(childRef.value.message)
})
</script>

<template>
  <Child ref="childRef" />
</template>

子组件通过 defineExpose 设置父组件可以通过 ref 获取的对象。必须为对象,否则会报错。

<!-- Child -->
<script setup>
defineExpose({
  msg: '来自子组件的消息'
})
</script>

<template>
  <div>我是子组件</div>
</template>

选项式还可以使用 、children(Vue3 不再支持) 等方式拿到组件实例。

选项式

选项式则可以通过 ref 直接拿到组件实例,和子组件的 this 效果一样,这样就能拿到组件实例的状态变量、方法等。

ref 会保存到 this.$refs 对象中。

父组件:

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

export default {
  components: { Child },
  mounted() {
    const msg = this.$refs.child.getMsg();
    console.log(msg)
  }
};
</script>

<template>
  <Child ref="child" />
</template>

子组件:

<!-- Child -->
<script>
export default {
  methods: {
    getMsg() {
      return '来自子组件的消息'
    }
  }
}
</script>

<template>
  <div>我是子组件</div>
</template>

EventBus

event bus 是事件总线的意思,底层是设计模式中的发布订阅模式。

监听者提供响应函数监听特定事件,当事件触发时,这个函数就会被执行,并带上参数,这样就能做到数据的通信。

发布订阅模式是非常常用的一种模块解耦后的通信方式。

Vue2 的组件实例是实现了 event bus 的,它有 $emit 和 $on 两个 API,前者触发事件,后者绑定事件函数。

const eventBus = new Vue();
// 在一个组件中绑定事件
eventBus.$on('countUpdate', (count) => { /* */ })
// 在另一个组件中触发事件,并提供参数
eventBus.$emit('countUpdate', count);

Vue3 后就不支持这套 API 了,需要自行安装发布订阅库。

provide / inject

provide 用于后代组件的数据透传,解决用 props 需要层层传递的麻烦写法。

React 中类似的概念是 context。

组合式写法

在父组件中,使用 provide 方法设置给后代使用的 key 和 value。

provide 方法可以多次调用设置不同的 key。同名的 key 后面的会覆盖前面的。

<!-- Parent -->
<script setup>
import { provide } from 'vue';
import Child from './child.vue';

provide('msg', '来自祖先组件的消息')
</script>

<template>
  <Child />
</template>

子组件通过 inject 拿到对应的 key,inject 的第一个参数是要获取的 key,第二个参数是可选的默认值(找不到对应 key 就用这个值)。

<!-- Child -->
<script setup>
import { inject } from 'vue';

const msg = inject('msg')
</script>

<template>
  <div>{{ msg }}</div>
</template>

如果 key 在 provide 中不存在,且没有提供默认值,Vue 会在控制台报警告 injection "key" not found.,然后 this.key 会获得一个 undefined。

选项式写法

父组件提供一个 provide 选项,可以是一个对象;也可以是是一个函数,其返回值需要是一个对象。

如果你需要用到 this,那就只能用函数,函数内的 this 会指向当前组件实例。如果你用对象,this 的值是 undefined。

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

export default {
  components: { Child },
  provide() {
    return {
      msg: '来自祖先组件的信息'
    }
  }
};
</script>

<template>
  <Child />
</template>

子组件通过 inject 拿到 provide 指定的对象。inject 通常为一个数组,指定需要用到的 key。

<!-- Child -->
<script>
export default {
  inject: ['msg'],
}
</script>

<template>
  <div>{{this.msg}}</div>
</template>

$attrs / $listeners

$attrs:一个包含了组件所有透传 attributes 的对象。指的是当前组件被调用时,传入的属性中,没有在 props 声明的其他的 key 的对象集合。(class 和 style 比较特殊,会进行合并)

$listeners:全部的 vue 事件集合。

Vue3 移除了 ,将其合并到了attrs` 中。下面说的是 Vue3 的写法。

然后我们配合 v-bind ,得到一个 v-bind="$attrs" 就能实现属性透传。

在选项模式下,直接通过 this.$attrs 拿到。在组合模式下,通过 const attrs = useAttrs() 拿到。

$attrs 相比 props 的优势在于,不用一个个 key 拿出来一个个传,直接传递 $attr 即可。但有个问题,就是这些属性会直接添加到到组件根 DOM 节点上,实在不怎么美观。如下:

<div id="app" data-v-app="">
  <div msg="[object Object]">
    <div msg="[object Object]">son</div>
  </div>
</div>

状态管理库

状态管理库,将 Vue 应用的需要进行共享的状态单独抽离出来,让组件的通信变得方便,在中大型项目已经非常常见。

Vue3 通常使用 Pinia,Vue2 在之前使用的则是 Vuex。它们都是 Vue 官方开发维护的库。

具体就不讲了,讲起来又是一堆文字。

其他

  • 将状态保存到 localStorage 里,所有的组件都能读写同一份数据
  • 通过改变 url 传递数据,比如加上 ?key=val

结尾

总结一下,组件通信的方式有:

  1. props:单向数据流,父传子;
  2. emit:通过事件的方式,子传父;
  3. ref:拿到子组件的组件实例或暴露出来的对象;
  4. event bus:利用 Vue2 的 on API,Vue3 不再支持,本质为发布订阅模式;
  5. provide / inject:注入给后代使用的数据;
  6. $attrs / $listeners:快捷的属性透传方式,但会污染真实 DOM 树;
  7. 状态管理库:通常为 Pinia 和 Vuex
© 版权声明
THE END