likes
comments
collection
share

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

作者站长头像
站长
· 阅读数 10

评论区有个同学让我总结一下 Vue 中的组件通信,虽然我觉得这可能是属于入门和基础必备,不再适合放入进阶系列,但是还是再好好总结一次,当做复习咯 ~

前言:吐槽一下,Vue 和 React 一个很大的不同就是,Vue 是当爹又当娘,给了你大量的所谓“语法糖”,生怕你脑子不够用。。。然后就造成一个问题:满屏的语法糖!各种的写法:vue2 写法、Option API 写法Composition API 写法,甚至 JavaScript 和 TypeScript 写法,各不相同,有种让人直呼 “小可爱” 的冲动。

React 则是把你当成捡来的,基本”生活物资“给你了,其余自给自足,突出一个灵活,但是通常都有最佳实践。。。

PS:这里没有好坏之分,各有各的好,不引战 —— 能解决问题的就是好技术! ~

所以,由于本人基本只用 TypeScript,所以本篇只关注 Composition + TypeScript(主要是和 React Hooks 基本可以无缝切换) 这种写法。

组件通信,无外乎下列三种情况:

  1. 父组件 ——> 子组件通信
  2. 子组件 ——> 父组件通信
  3. 跨组件通信

PS:本系列只涉及到 Vue 3 中的新内容,所以下面我介绍的都是基于 Vue 3 Composition API 中的实现,没有去总结那些已经不常用或者被废弃的冷知识了,本文中的这些技巧已经足够绝大部分场景使用了。

1. 父 ——> 子 通信

1.1 传递状态:defineProps

当使用 <script setup> 时,defineProps() 宏函数支持从它的参数中推导类型:

<script setup lang="ts">
const props = defineProps({
  foo: { type: String, required: true },
  bar: Number
})

props.foo // string
props.bar // number | undefined
</script>

这被称之为“运行时声明”,因为传递给 defineProps() 的参数会作为运行时的 props 选项使用。

然而,通过泛型参数来定义 props 的类型通常更直接:

<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

这被称之为“基于类型的声明”。编译器会尽可能地尝试根据类型参数推导出等价的运行时选项。在这种场景下,我们第二个例子中编译出的运行时选项和第一个是完全一致的。

基于类型的声明或者运行时声明可以择一使用,但是不能同时使用。

我们也可以将 props 的类型移入一个单独的接口中:

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>

上一篇文章有讲过,父子传递数据比较简单,就是通过 defineProps 传递数据(静态数据或者动态数据):

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

简单来个代码示例:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

如果你想在父组件传入的值发生更改时调用子组件内部的方法,要怎么做?简单,用 watch:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

这样,每次父组件传入值改变时,子组件就会调用 callMe 方法。来试试:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

另外,还可以在 defineProps 中做更多的事情:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

默认值:withDefaults

当使用基于类型的声明时,我们失去了为 props 声明默认值的能力。这可以通过 withDefaults 编译器宏解决:

export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

这将被编译为等效的运行时 props default 选项。此外,withDefaults 帮助程序为默认值提供类型检查,并确保返回的 props 类型删除了已声明默认值的属性的可选标志。

1.2 传递方法:defineEmits

上一篇文章有讲过,使用 defineEmits

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

测试:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

2. 子组件 ——> 父组件通信

熟悉 Vue 2 的同学应该知道,在 Vue 2 中的 $refs 属性也可以访问子组件的内容。例如:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

但是,使用 $refs 有一些问题:

  1. 紧耦合:父组件可以访问子组件的任何内容,即使这些内容是不希望被暴露出去的。如果子组件改变了其内部逻辑,这可能会导致奇怪的副作用或错误。

  2. 类型不安全,因为 $refs 属性是一个通用对象,没有任何类型信息或代码补全。这就很容易造成错误或访问不存在的内容。

defineExpose

Vue 3 新推出了一个内置的宏:defineExpose(),一个可以在组件的 setup() 函数中调用的函数。参数是要暴露的内容。是这样用的:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

来看看效果:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

它带来的好处是:

  1. 使父组件和子组件之间的连接清晰明确。子组件声明它所暴露的内容,而父组件只能访问所暴露的内容。

  2. 提高类型安全。暴露的内容是有类型信息或代码补全,从而减少出错的几率,提升了开发体验。

3. 跨组件通信

3.1 mitt

首先可能会想到 eventBus,先来看看 Vue2 中熟悉的用法:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

到 Vue 3 中,$on$off$once 方法已从 Vue 实例中完全删除,而且 vue3 中没有Vue构造函数,也就没有 Vue.prototype 以及组合式API写法没有 this。因此,要使用 eventBus 模式,就需要安装一个外部事件发射器和监听器包,常用的比如 mitt

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

在组件中使用:

<script setup lang="ts">
// import ParentComponent from "./components/ParentComponent.vue";
import ComponentA from "./components/ComponentA.vue";
import ComponentB from "./components/ComponentB.vue";
</script>

<template>
  <!-- <ParentComponent /> -->
  <ComponentA />
  <ComponentB />
</template>

<style scoped></style>

试试:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

3.2 依赖注入:provide + inject

类似于 React 中的 Context,只是用起来比 React 稍微简单点而已,主要用在跨层级组件通信传值~

  • provide() 接受两个参数:第一个参数是要注入的 key,可以是一个字符串或者一个 symbol,第二个参数是要注入的值。

  • 当使用 TypeScript 时,key 可以是一个被类型断言为 InjectionKey 的 symbol。InjectionKey 是一个 Vue 提供的工具类型,继承自 Symbol,可以用来同步 provide() 和 inject() 之间值的类型。

与注册生命周期钩子的 API 类似,provide() 必须在组件的 setup() 阶段同步调用。

还可以传 Symbol 类型的值:

<script setup>
import { ref, provide } from 'vue'
import { fooSymbol } from './injectionSymbols'

// 提供静态值
provide('foo', 'bar')

// 提供响应式的值
const count = ref(0)
provide('count', count)

// 提供时将 Symbol 作为 key
provide(fooSymbol, count)
</script>

取值:

<script setup>
import { inject } from 'vue'
import { fooSymbol } from './injectionSymbols'

// 注入不含默认值的静态值
const foo = inject('foo')

// 注入响应式的值
const count = inject('count')

// 通过 Symbol 类型的 key 注入
const foo2 = inject(fooSymbol)

// 注入一个值,若为空则使用提供的默认值
const bar = inject('foo', 'default value')

// 注入一个值,若为空则使用提供的函数类型的默认值
const fn = inject('function', () => {})

// 注入一个值,若为空则使用提供的工厂函数
const baz = inject('factory', () => new ExpensiveObject(), true)
</script>

来看一个例子:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03

效果:

【Vue3】保姆级毫无废话的进阶到实战教程 - 03