图解 Vue 3 组件通信:红楼梦剧情演绎版

887 阅读9分钟

生拉硬扯:从红楼梦到 Vue

前几天再读《红楼梦》,贾家宁荣二府人物关系错综复杂,跟前端项目中的组件嵌套层级关系类似。这里尝试用红楼梦剧情演绎,辅以示意图解,新瓶旧酒,梳理一下 Vue 3 中的组件通信。

c-1.png

如上图示,贾家从创业到抄家,前后一共历经五世:

  • 水字辈,如贾演、贾源
  • 代字辈,如贾代化、贾代善
  • 文字辈,如贾敬、贾赦、贾政等
  • 玉字辈,如贾珍、贾琏、贾宝玉等
  • 草字辈,如贾蓉、贾兰等

前端单页应用项目,为了业务拆解和逻辑复用,会采用组件式开发。Vue 框架将内容、逻辑和样式一起封装在单文件组件内部,并采用层层嵌套的方式构建整个 UI 界面。这种层层嵌套的树状组件结构,和贾家各代人物关系类似。

c-2.png

常规的 Vue 3 单页应用,其组件层级从上到下大概为:

  • 根组件 App.vue
  • 一级路由容器组件 RouterView
  • 页面组件
  • 公共组件、业务组件等
  • 更细拆分的子组件

于是,这里将红楼梦贾家人物关系搬到了 Vue 项目中,把每个人物当作组件,人物和组件共用相同的父子关系。

c-3.jpg

亲子鉴定:Vue 的父子组件关系

这里的“父子”(以及后面的爷孙)是个统称,也涵盖了母女、母子、父女等关系。称之为“Vue 母女组件”也未尝不可。

另外,父子关系是相对的,例如贾代善_贾母 -> 贾政_王夫人 -> 贾宝玉,贾政既是贾宝玉的父组件,又是贾母的子组件。

通过 Vue 3 组合式 API <script setup>,在父组件中导入一个子组件后便可以使用。

如果采用选项式 API,父组件导入子组件后,还需要通过 components 选项对子组件进行注册才能使用。

当然,全局注册一个组件,它不需要导入即可在当前应用的任何组件中直接使用。

<!-- JiaZhengWangFuren.vue -->

<template>
<div class="贾政_王夫人">
  <JiaBaoyu />
</div>
</template>

<script setup lang="ts" name="贾政_王夫人">
// 引入贾宝玉组件
import JiaBaoyu from './JiaBaoyu.vue';
</script>

上面代码为贾政_王夫人组件 JiaZhengWangFuren.vue,用 ES6 模块语法 import 引入贾宝玉组件 JiaBaoyu.vue 并使用。这样的行为,就构成了 Vue 最基本的组件关系:

  • 贾政_王夫人 <JiaZhengWangFuren /> 是贾宝玉 <JiaBaoyu /> 的父组件
  • 贾宝玉 <JiaBaoyu /> 则是贾政_王夫人 <JiaZhengWangFuren /> 的子组件

下图左侧为组件父子关系,右侧为组件 DOM 嵌套关系。

c-4.png

注:下面的描述,代码均以组合式 API 语法 <script setup> 呈现,特殊情况下使用选项式 API 时会单独说明。文字描述中会直接使用中文人名称呼组件,不再赘述拼音文件名或者标签。

插槽、组件:傻傻分不清楚

红楼梦中,探春妹妹和贾宝玉关系亲密,她每次让出门逛街的宝玉替她捎东西,都会给宝玉做一双费时费工的漂亮鞋子。

下面代码,贾探春组件作为子标签,嵌套在贾宝玉组件父标签中。

<!-- JiaZhengWangFuren.vue -->

<template>
<div class="贾政_王夫人">
  <JiaBaoyu>
    <JiaTanchun />
  </JiaBaoyu>
</div>
</template>

<script setup lang="ts" name="贾政_王夫人">
// 引入贾宝玉组件
import JiaBaoyu from './JiaBaoyu.vue';
// 引入贾探春组件
import JiaTanchun from './JiaTanchun.vue';
</script>

这样的组件标签嵌套结构,会让初学者将贾探春误认为贾宝玉的子组件。

大误!

从组件关系来看,贾探春和贾宝玉应该是兄弟/妹组件才对,是同级关系,他俩都是贾政_王夫人的子组件。

而这里的标签嵌套,是 Vue 中插槽 slot 的概念,只改变组件的 DOM 渲染位置,而不会影响组件的伦理关系。

二哥哥贾宝玉和探春妹妹并排站着也好,还是哥哥将妹妹裹在自己的狐皮袄子里也好,无论如何也改变不了他们俩是兄妹的关系。

交际的艺术:红楼人物组件通信

理清 Vue 最基本的组件关系定义,避免将插槽和子组件概念混淆后,下面是本文的重点内容组件通信。

默认情况下,每个组件内聚独立,只能使用自己内部的数据。而前端复杂业务逻辑的实现,必然需要将不同的组件相互组合或串联起来,不同组件之间不可避免地需要传递或共享一些数据。

不同组件之间相互传递或共享数据/状态,称为组件通信。

从组件伦理关系上区分,大概有这么几种情况,最常见的就是父子组件通信、再复杂些的是爷孙跨层级通信、较麻烦的是任意组件之间的通信。

1. props:父传子

定义:

父组件使用 props 将数据传递给子组件,子组件需要用 defineProps 宏显式声明它所接受的 props。子组件声明的 props 会自动暴露给模板直接插值绑定使用。

c-5.png

约定:

在子组件中声明 props 时,它们是合法的 JavaScript 标识符,应使用驼峰形式;而当父组件向子组件传递 props 时,为了和 HTML attribute 对齐,通常写成小写字母加连字符的格式。Vue 编译时会自动做统一处理。

props 要遵循单向数据流原则,子组件禁止修改 props!

父组件传给子组件的 props 是响应式的,父组件更新 props,子组件可以检测到 props 的变化,自动进行视图更新。

示例:

下面代码实现了父组件贾政向子组件贾宝玉传递数据:

<!-- 父组件 JiaZhengWangFuren.vue -->

<template>
<div class="贾政_王夫人">
  <button
    @click="isFatherAtHome = false"
  >去上朝</button>
  <JiaBaoyu
    :is-play-with-girl="!isFatherAtHome"
    :father-gifts="['无知的孽障', '作死的畜生']"
  />
</div>
</template>

<script setup lang="ts" name="贾政_王夫人">
import { ref } from 'vue';
import JiaBaoyu from './JiaBaoyu.vue';

const isFatherAtHome = ref(true);
</script>
<!-- 子组件 JiaBaoyu.vue -->

<template>
<div class="贾宝玉">
  <p v-if="isPlayWithGirl">与丫鬟厮混</p>
  <p>父亲的教诲:{{ fatherWords }}</p>
</div>
</template>

<script setup lang="ts" name="贾宝玉">
import { computed } from 'vue';

const props = defineProps<{
  isPlayWithGirl: boolean;
  fatherGifts: string[];
}>();

const fatherWords = computed(
  () => props.fatherGifts.join('、')
);
</script>

c-6.gif

子组件贾宝玉中使用 defineProps 声明可以接受的 props 为 isPlayWithGirl 和 fatherGifts,父组件贾政在子组件标签中使用属性将数据传入。

恨铁不成钢的贾政送给贾宝玉的礼物是严苛的斥责,而贾宝玉常在内帏厮混,当父亲贾政在家时才有所忌惮收敛。

2. emit:子传父

定义:

子组件通过 defineEmits 宏声明需要抛出的事件,它返回 emit 函数。子组件调用该 emit 方法,通过传入自定义事件名称来抛出一个事件。父组件通过 v-on 或 @ 选择性地监听子组件上抛/派发的自定义事件,并执行事件处理函数或者说是回调函数。

子组件如果需要在抛出/触发事件时附带额外数据传递给父组件,给 emit 提供额外的参数即可。父组件的事件处理函数的参数会收到这些额外的数据。

c-7.png

注:这里的 emit、抛出、上抛、派发、触发都是同一个概念的不同称呼。

约定:

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件,$emit 是组件内置的,等同于 defineEmits 的返回值。

同组件的 props 一样,事件的名称也提供了自动格式转换。如果派发了驼峰形式命名的事件,父组件仍然可以使用小写字母加连字符的形式来监听。

和原生 DOM 事件不同,组件触发的自定义事件没有冒泡机制。

父组件中监听事件,如果编写内联的箭头函数作为监听器,该函数默认参数即为接收到事件附带的额外数据。如果直接编写执行的内联 JavaScript 语句,内置的 $event 变量同样表示事件的附带数据参数。

示例:

下面代码实现了子组件贾宝玉向父组件贾政传递数据:

<!-- 子组件 JiaBaoyu.vue -->

<template>
<div class="贾宝玉">
  <button @click="writePoem">题一联</button>
  <p v-if="poemIndex > -1">{{ poemList[poemIndex] }}</p>
</div>
</template>

<script setup lang="ts" name="贾宝玉">
import { ref } from 'vue';

const emit = defineEmits<{
  (e: 'write-poem', i: number): void
}>();

const poemIndex = ref(-1);
const poemList = [
  '绕堤柳借三篙翠,隔岸花分一脉香',
  '宝鼎茶闲烟尚绿,幽窗棋罢指犹凉',
  '新涨绿添浣葛处,好云香护采芹人',
  '吟成荳蔻才犹艳,睡足酴醿梦也香'
];

const writePoem = () => {
  poemIndex.value = ++poemIndex.value % poemList.length;
  emit('write-poem', poemIndex.value);
};
</script>
<!-- 父组件 JiaZhengWangFuren.vue -->

<template>
<div class="贾政_王夫人">
  <JiaBaoyu @write-poem="giveFeedback" />
  <p v-if="feedbackIndex > -1">
    贾政{{ feedbackList[feedbackIndex] }}
  </p>
</div>
</template>

<script setup lang="ts" name="贾政_王夫人">
import { ref } from 'vue';
import JiaBaoyu from './JiaBaoyu.vue';

const feedbackIndex = ref(-1);
const feedbackList = [
  '听了,点头微笑',
  '摇头说道:也未见长',
  '听了,摇头说:更不好',
  '笑道:这是套的“书成蕉叶文犹绿”,不足为奇'
];

const giveFeedback = (index: number) => {
  feedbackIndex.value = index;
};
</script>

c-8.gif

为贾元春省亲而修建的大观园竣工时,贾政借着题匾额及作对联,考验贾宝玉的才学。

子组件贾宝玉使用 defineEmits 声明需要抛出的事件名称为 write-poem,抛出事件时额外携带的数据为数字类型的下标。父组件贾政监听该自定义事件,事件的处理函数为 giveFeedback。当父组件贾政监听到子组件贾宝玉触发了作对联事件时,就会执行相应的处理函数给出评价。

贾政貌似严厉冷酷,实则宽容耐心引导有方,几次对贾宝玉对联的贬损表现了贾政威严之下的良苦用心。

3. v-model:父子同步语法糖

定义:

在组件上使用 v-model 以实现双向绑定。可以将双向绑定理解为父组件和子组件的双向通信,父组件将某项数据传递给子组件,子组件将一个新数据传回父组件,并将父组件传过来的数据同步做修改。通常将 v-model 用在自定义表单项组件上。

v-model 是 modelValue prop 和 update:modelValue 自定义事件的语法糖。

c-9.png

约定:

modelValue 是默认的 prop 变量名,通过给 v-model 指定一个参数来更改它。例如 v-model:name,这样会将 prop 属性名由 modelValue 改成 name,并将自定义事件名由 update:modelValue 改成 update:name。通过这个约定,可以在组件上实现多个 v-model 的绑定。

示例:

下面代码实现了父组件林如海和子组件林黛玉的双向通信:

<!-- 父组件 JiaMinLinRuhai.vue -->

<template>
<div class="贾敏_林如海">
  <p>财富:{{ wealth }}两</p>
  <LinDaiyu v-model="wealth" />
</div>
</template>

<script setup lang="ts" name="贾敏_林如海">
import { ref } from 'vue';
import LinDaiyu from './LinDaiyu.vue';

const wealth = ref(3000000);
</script>
<!-- 子组件 LinDaiyu.vue -->

<template>
<div class="林黛玉">
  <button @click="pay">吃穿用度</button>
</div>
</template>

<script setup lang="ts" name="林黛玉">
const props = defineProps<{
  modelValue: number
}>();
const emit = defineEmits<{
  (e: 'update:modelValue', v: number): void
}>();

const pay = () => {
  emit(
    'update:modelValue',
    props.modelValue - 10000
  );
};
</script>

其中的 v-model 等价于:

<LinDaiyu
  :model-value="wealth"
  @update:model-value="newValue => wealth = newValue"
/>

c-10.gif

子组件林黛玉中使用 defineProps 声明可以接受的 props 为 modelValue,并使用 defineEmits 声明需要抛出的事件名称为 update:modelValue。这样就可以在父组件林如海中,使用 v-model 双向绑定财富变量,子组件控制父组件变量的同步修改。

林如海膝下无子,对独生女林黛玉爱如珍宝。林家积累的数目可观的财富,自然都会留给林黛玉支配。

4. 回调函数 props:子传父

定义:

Vue 组件可以接收函数类型的 props,父组件将回调函数使用 props 传入子组件,子组件通过调用父组件的函数/方法进行传值。这种方式和 React 的工作模式类似。

c-11.png

示例:

下面代码实现了子组件贾政向父组件贾母传递数据:

<!-- 父组件 JiaDaiShanJiaMu.vue -->

<template>
<div class="贾代善_贾母">
  <p>灯谜:猴子身轻站树梢(打一果名)</p>
  <p v-if="isAnswerCorrect">谜底“荔枝”被猜中了</p>
  <JiaZhengWangFuren :guess="onGuess" />
</div>
</template>

<script setup lang="ts" name="贾代善_贾母">
import { ref } from 'vue';
import JiaZhengWangFuren from './JiaZhengWangFuren.vue';

const isAnswerCorrect = ref(false);

const onGuess = (answer: string) => {
  isAnswerCorrect.value = (answer === '荔枝');
};
</script>
<!-- 子组件 JiaZhengWangFuren.vue -->

<template>
<div class="贾政_王夫人">
  <button @click="guess('荔枝')">猜灯谜</button>
</div>
</template>

<script setup lang="ts" name="贾政_王夫人">
defineProps<{
  guess: (answer: string) => void
}>();
</script>

c-12.gif

贾府庆元宵猜灯谜,贾母做灯谜让贾政猜:“猴子身轻站树梢(打一果名)”,贾政明知谜底是荔枝,仍然故意乱猜别的让母亲高兴。承欢膝下,其乐融融。

父组件贾母使用 props 将自己的内部方法 onGuess 传给子组件贾政,子组件贾政调用该方法,通过参数将数据传递给父组件。

5. props + emit:爷孙跨层级相互通信

定义:

在多层级嵌套的组件树中,某个深层的子组件需要一个较远的祖先组件中的部分数据,如果使用 props 则必须将其沿着组件链逐级传递下去;而子组件如果需要将事件派发给较远的祖先组件,使用 emit 同样也需要沿着组件链逆向逐级上抛传递。

c-13.png

示例:

下面代码实现了爷组件贾演和孙组件贾珍互相传递数据:

<!-- 爷组件 JiaYan.vue -->

<template>
<div class="贾演">
  <p>
    宁国公
    <span v-if="showIncense">焚香点烛</span>
  </p>
  <JiaDaihua
    noble-title="爵位"
    @worship="worship"
  />
</div>
</template>

<script setup lang="ts" name="贾演">
import { ref } from 'vue';
import JiaDaihua from './JiaDaihua.vue';

const showIncense = ref(false);
const worship = () => showIncense.value = true;
</script>
<!-- 父组件 JiaDaihua.vue -->

<template>
<div class="贾代化">
  <p>袭{{ nobleTitle }}</p>
  <JiaJing
    :noble-title="nobleTitle"
    @worship="worship"
  />
</div>
</template>

<script setup lang="ts" name="贾代化">
import JiaJing from './JiaJing.vue';

defineProps<{
  nobleTitle: string
}>();
const emit = defineEmits<{
  (e: 'worship'): void
}>();

const worship = () => emit('worship');
</script>
<!-- 子组件 JiaJing.vue -->

<template>
<div class="贾敬">
  <p>袭{{ nobleTitle }}</p>
  <JiaZhenYouShi
    :noble-title="nobleTitle"
    @worship="worship"
  />
</div>
</template>

<script setup lang="ts" name="贾敬">
import JiaZhenYouShi from './JiaZhenYouShi.vue';

defineProps<{
  nobleTitle: string
}>();
const emit = defineEmits<{
  (e: 'worship'): void
}>();

const worship = () => emit('worship');
</script>
<!-- 孙组件 JiaZhenYouShi.vue -->

<template>
<div class="贾珍_尤氏">
  <p>袭{{ nobleTitle }}</p>
  <button @click="worship">祭祖</button>
</div>
</template>

<script setup lang="ts" name="贾珍_尤氏">
defineProps<{
  nobleTitle: string
}>();
const emit = defineEmits<{
  (e: 'worship'): void
}>();

const worship = () => emit('worship');
</script>

c-14.gif

贾家宁国公和荣国公的后代是世袭的贵族。宁国公贾演死后,由其长子贾代化降等袭爵为一等神威将军;贾代化死后,由其次子贾敬袭爵;贾敬最终出家做了道士,由其长子贾珍降等袭爵为三品爵威烈将军。

使用 props 将宁国公爵位 nobleTitle 沿着贾演 -> 贾代化 -> 贾敬 -> 贾珍的顺序逐级透传下来;孙组件贾珍祭祖时,使用 emit 将 worship 事件由下至上逐级上抛。

6. provide + inject:爷孙跨层级相互通信

定义:

props 逐级透传和 emit 逐级上抛实现爷孙组件通信时,如果组件链路较长,会影响到诸多中间组件,未消费使用属性和事件的中间组件,也需要重复性地定义、声明、传递,操作起来琐碎麻烦。

Vue 的依赖注入 provide 和 inject 可以帮助我们解决这一问题。一个父组件相对于其所有的后代组件,会作为依赖提供者,用 provide 函数为组件后代提供数据。任意一个后代组件,无论层级有多深,都可以用 inject 函数注入由父组件提供给整条链路的依赖,消费使用其传递下来的数据和方法,借助回调函数的参数,后代组件可以将数据传回父组件。

c-15.png

示例:

下面代码实现了父组件贾演和后代组件贾珍互相传递数据:

<!-- 父组件 JiaYan.vue -->

<template>
<div class="贾演">
  <p>
    宁国公
    <span v-if="showIncense">焚香点烛</span>
  </p>
  <JiaDaihua />
</div>
</template>

<script setup lang="ts" name="贾演">
import { ref, provide } from 'vue';
import JiaDaihua from './JiaDaihua.vue';

const showIncense = ref(false);
const worship = () => showIncense.value = true;

provide('family-info', {
  nobleTitle: '爵位',
  worship
});
</script>
<!-- 后代组件 JiaZhenYouShi.vue -->

<template>
<div class="贾珍_尤氏">
  <p>袭{{ nobleTitle }}</p>
  <button @click="worship">祭祖</button>
</div>
</template>

<script setup lang="ts" name="贾珍_尤氏">
import { inject } from 'vue';

interface IFamilyInfo {
  nobleTitle: string;
  worship(): void;
}

const {
  nobleTitle,
  worship
} = inject('family-info') as IFamilyInfo;
</script>

使用依赖注入实现的效果和逐级透传实现的效果完全一致,优点是代码精简、逻辑清晰。

7. ref + defineExpose:父子组件相互通信

定义:

ref 是一个特殊的 attribute,允许我们在特定的 DOM 元素或子组件实例被挂载后,获得对它的直接引用。使用了 <script setup> 定义的组件是默认私有的,只有子组件通过 defineExpose 宏显式地暴露出的属性和方法,父组件才可以访问。

c-16.png

约定:

父组件通过模板引用获取到子组件的实例时,子组件暴露出的 ref 类型数据都会自动解包,取值和赋值均不需要加 .value。

如果一个子组件使用的是选项式 API 或没有使用 <script setup>,被引用的组件实例和该子组件的 this 完全一致,这意味着父组件对子组件的所有属性和方法都有完全的访问权。

父组件如果使用选项式 API,模板引用使用 this.$refs.** 来获取。

示例:

下面代码实现了父组件王熙凤和子组件巧姐互相传递数据:

<!-- 父组件 JiaLianWangXifeng.vue -->

<template>
<div class="贾琏_王熙凤">
  <button @click="cureChickenpox">给巧姐治疗水痘</button>
  <button @click="doGoodDeeds">给巧姐积福报</button>
  <QiaoJie ref="child" />
</div>
</template>

<script setup lang="ts" name="贾琏_王熙凤">
import { ref } from 'vue';
import QiaoJie from './QiaoJie.vue';

const child = ref<InstanceType<typeof QiaoJie> | null>(null);

const cureChickenpox = () => {
  child.value!.isChickenpoxCured = true;
};
const doGoodDeeds = (() => {
  const list = [
    '请刘姥姥进屋',
    '给刘姥姥二十两银子',
    '送刘姥姥一吊钱雇车子'
  ];
  let i = 0;
  return () => {
    if (i === list.length) return;
    child.value?.doGoodDeeds(list[i++]);
  };
})();
</script>
<!-- 子组件 QiaoJie.vue -->

<template>
<div class="巧姐">
  <p>水痘{{ isChickenpoxCured ? '已' : '未' }}痊愈</p>
  <transition name="msg">
    <p v-if="showMsg">{{ goodDeeds.at(-1) }},福报 +1</p>
  </transition>
</div>
</template>

<script setup lang="ts" name="巧姐">
import { ref, reactive } from 'vue';

const showMsg = ref(false);
const isChickenpoxCured = ref(false);
const goodDeeds: Array<string> = reactive([]);
const doGoodDeeds = (deed: string) => {
  goodDeeds.push(deed);
  showMsg.value = true;
  setTimeout(() => showMsg.value = false, 600);
};

defineExpose({
  isChickenpoxCured,
  doGoodDeeds
});
</script>

<style lang="sass">
.msg-enter-from
  transform: translateY(100%)
  opacity: 0
.msg-leave-to
  transform: translateY(-100%)
  opacity: 0
.msg-enter-active, .msg-leave-active
  transition: .6s
</style>

c-17.gif

父组件王熙凤先声明一个名为 child 的 ref 变量来存放子组件的引用,并将该变量使用 ref="child" 绑定到子组件巧姐标签上。child 变量初始值为空,在子组件挂载后才能访问。

子组件巧姐用 defineExpose 暴露出了水痘是否痊愈属性 isChickenpoxCured 和积福报方法 doGoodDeeds。父组件王熙凤通过 child.value 访问/修改子组件巧姐的属性,并传参调用其方法。

巧姐自出生起,便娇贵多病,王熙凤的许多心神都耗费在了上面。巧姐感染了天花发水痘,王熙凤通过延医请药、供奉神灵,保巧姐度过了危险。

刘姥姥度日艰难进大观园,受到贾府的帮助。当贾府遭难时,正是刘姥姥不忘旧恩将巧姐搭救出了苦海。

8. 事件总线:任意组件相互通信

定义:

创建一个全局的事件总线,A 组件在事件总线上注册/监听事件,B 组件在事件总线上触发相同的事件,通过执行A组件注册事件时的回调函数,实现组件通信。

在 Vue2 中借助一个公共组件实例 new Vue() 上的 $on、$off 和 $emit方法实现。

Vue 3 移除了 $on 和 $off 等实例方法,通过导入三方库实现,例如 mitt

c-18.png

约定:

在 onMounted 组件挂载钩子中注册事件,并在 onBeforeUnmount 组件销毁前使用 off 注销/解绑事件,防止重复注册。

示例:

下面代码实现了 B 组件秦可卿向 A 组件王熙凤传递数据:

// 事件总线 dream.ts

import mitt from 'mitt';
export default mitt();
<!-- A 组件 JiaLianWangXifeng.vue -->

<template>
<div class="贾琏_王熙凤" style="padding: 15px;">
  <p v-if="secret">{{ secret }}</p>
</div>
</template>

<script setup lang="ts" name="贾琏_王熙凤">
import { ref, onMounted, onBeforeUnmount } from 'vue';
import dream from '../utils/dream';

const secret = ref('');
const onLastWords = (v: string) => secret.value = v;

onMounted(() => dream.on('last-words', onLastWords));
onBeforeUnmount(() => dream.off('last-words', onLastWords));
</script>
<!-- B 组件 JiaRongQinKeqing.vue -->

<template>
<div class="贾蓉_秦可卿">
  <button @click="send">托梦给王熙凤</button>
</div>
</template>

<script setup lang="ts" name="贾蓉_秦可卿">
import dream from '../utils/dream';

const send = () => dream.emit(
  'last-words',
  '婶婶,你是脂粉堆里的英雄,连那些束带顶冠的男子也不能过你'
);
</script>

c-19.gif

王熙凤是荣国府大管家,秦可卿是宁国府大管家,两人之间既是婶子与侄媳妇的关系,也是惺惺相惜的知己。秦可卿临死前托梦给王熙凤,指出贾府气数将尽,提醒王熙凤要居安思危、早做准备。

A 组件王熙凤在事件总线上注册 last-words 事件,并指定回调函数是 onLastWords,B 组件秦可卿在事件总线上触发事件 onLastWords,用第二个参数传递遗言。事件总线上监听到该事件时,执行 A 组件王熙凤中的方法 onLastWords,参数即为 B 组件秦可卿传递过来的遗言数据。

9. Pinia:任意组件相互通信

定义:

Pinia 是 Vue 的专属状态管理库,支持跨组件共享状态。

执行 createPinia 创建 pinia 实例,传递给根应用 app。

使用 defineStore 方法定义 Store,第一个参数是独一无二的名字,第二个参数是 Setup 函数。在 Setup 函数中定义一些响应式属性和方法,然后将需要暴露出去的属性和方法放在对象中返回。

在组件中使用 Store 时,将上面定义好的 Store 导入执行,得到实例化的 store,这样就可以使用 store 中的响应式属性和方法。

c-20.png

约定:

为了从 store 中提取属性时保持其响应性,需要使用 storeToRefs。

示例:

下面代码实现了 A 组件贾宝玉和 B 组件林黛玉互相传递数据:

// 应用入口 main.ts

import { createApp } from 'vue';
import { createPinia } from 'pinia';
import JiaFamily from './components/JiaFamily.vue';

const pinia = createPinia();
const app = createApp(JiaFamily);

app.use(pinia).mount('#app');
// 定义 Store use-love.ts

import { defineStore } from 'pinia';
import { ref } from 'vue';

const useLove = defineStore('love', () => {
  const loveForJiaBaoyu = ref(0);
  const loveForLinDaiyu = ref(0);
  return {
    loveForJiaBaoyu,
    loveForLinDaiyu
  };
});

export default useLove;
<!-- A 组件 JiaBaoyu.vue -->

<template>
<div class="贾宝玉">
  <p>林妹妹的爱:{{ loveForJiaBaoyu }}</p>
  <button @click="loveForLinDaiyu++">向黛玉表白</button>
</div>
</template>

<script setup lang="ts" name="贾宝玉">
import { storeToRefs } from 'pinia';
import useLove from '../store/use-love';

const store = useLove();
const { loveForJiaBaoyu, loveForLinDaiyu } = storeToRefs(store);
</script>
<!-- B 组件 LinDaiyu.vue -->

<template>
<div class="林黛玉">
  <p>宝哥哥的爱:{{ loveForLinDaiyu }}</p>
  <button @click="loveForJiaBaoyu++">向宝玉示爱</button>
</div>
</template>

<script setup lang="ts" name="林黛玉">
import { storeToRefs } from 'pinia';
import useLove from '../store/use-love';

const store = useLove();
const { loveForJiaBaoyu, loveForLinDaiyu } = storeToRefs(store);
</script>

c-21.gif

将 pinia 实例传递给贾家应用。定义了公共 store,将 A 组件贾宝玉给 B 组件林黛玉的爱 loveForLinDaiyu,以及 B 组件林黛玉给 A 组件贾宝玉的爱 loveForJiaBaoyu 两个状态统一管理。在两个组件中实例化 store,使用其状态即可,A 组件修改了 B 组件使用的状态,B 组件会自动刷新视图,反之亦然。

“任凭弱水三千,我只取一瓢饮”,你证我证,心证意证,心心相印!

拾漏补遗:未尽述的组件通信方式

Vue 3 组件通信方式,除了上面列出 9 种外,还有以下旧的或者不常规的:

  1. 尽管 Pinia 已经成为官方推荐的状态管理解决方案,Vuex 4 仍然可以在 Vue 3 中使用,它引入了新的 useStore 组合式函数来使用 store。和 Pinia 一样,Vuex 也可以用在任意组件之间相互通信。

  2. 透传 attribute,它指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器,在组件中用 useAttrs 获取父组件透传过来的所有 attribute。父组件可以用透传 attribute 传递数据给子组件,子组件通过调用父组件透传过来的 v-on 事件,也可以将数据传递给父组件。

  3. 作用域插槽,子组件定义 slot 插槽时,在 slot 标签上用类似于 props 的方式传递数据,这些 props 会作为子组件 v-slot 指令的值,可以在插槽内的父组件模板内容中使用。作用域插槽可以用来将子组件数据传递给父组件中的部分模板。

  4. 组件实例,在选项式 API 中,组件可以通过 this.$parent 访问父组件实例,用 this.$root 访问根组件实例,利用这些属性结合 this.$refs 实现父子、爷孙、或者组件树中的任意组件之间的通信。

最后总结:欢迎赞评藏

满纸荒唐言,一把辛酸泪。都云作者痴,谁解其中味?

本文字牵强附会,借红楼梦之剧情,大致对 Vue 3 中的各类组件通信简要做了阐释。

props 和 emit 是 Vue 框架组件通信的基石,v-model 是 props 和 emit 结合使用的语法糖,常用在表单项组件中;依赖注入 provide 和 inject 解决了爷孙组件跨层级相互通信时链路较长的问题;ref 是组件实例的引用,通过组件实例可以直接读取其内部属性或调用其内部方法;而传统的事件总线,以及官方更推荐的 Pinia 公共状态管理,可以解决任意组件之间的相互通信难题。

在前端项目中,开发者需根据具体的业务场景选择合适的方式。