今天讲讲 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
结尾
总结一下,组件通信的方式有:
- props:单向数据流,父传子;
- emit:通过事件的方式,子传父;
- ref:拿到子组件的组件实例或暴露出来的对象;
- event bus:利用 Vue2 的 on API,Vue3 不再支持,本质为发布订阅模式;
- provide / inject:注入给后代使用的数据;
$attrs
/$listeners
:快捷的属性透传方式,但会污染真实 DOM 树;- 状态管理库:通常为 Pinia 和 Vuex