# 组件间通信

无论是 Vue 还是 React,都是把一个 SPA 拆分成多个 components,将其按照一定的风格与样式进行组装而形成一整个页面,各个组件之间有其相对独立的数据与状态。

Vue 相较于 React 而言,它的各个组件之间的独立性更强大;React 由于是数据(状态)驱动视图的更新,并且组件之间是呈现一种 “组件树” 的关系,因此所谓的组件父子关系在 React 中体现的更加明显,相应的组件之间的数据传递也是重中之重的功能。

在 Vue 里面,说起父传子我们想起 Props ,说起子传父我们想起 Emits ,说起兄弟之间传参我们就想到利用一个公共组件等等,而这些在 React 当中几乎都有体现。不过相较于 Vue,React 的实现方式可能更偏向原始。

# React 父传子 ——props

和 Vue 一样,你如果父组件给子组件传数据,也是直接在 html 标签内部写 属性名="属性值" 即可;相应的,子组件需要一一对应所传递的属性来接收。

const { useState } = require("react");
// 子组件
// 1. 通过 props 接收父组件传递的数据
/* function Child (props) {
  return (
    <div>
      <div > 我是子组件!!!我接收到了以下数据↓</div>
      <div>{props.name}</div>
      <div>{props.age}</div>
    </div>
  );
} */
// 2. 通过解构赋值接收父组件传递的数据
/* function Child ({name, age}) {
  return (
    <div>
      <div > 我是子组件!!!我接收到了以下数据↓</div>
      <div>{name}</div>
      <div>{age}</div>
    </div>
  );
} */
// 3. 拿到 children
function Child({ name, age, func, children }) {
  return (
    <div>
      <div>我是子组件!!!我接收到了以下数据↓</div>
      <div>{name}</div>
      <div>{age}</div>
      <div>{children}</div>
      <button onClick={func}>点击我,调用父组件传递的函数</button>
    </div>
  );
}
// 父组件
function Father() {
  const [user] = useState({ name: "张三", age: 18 });
  return (
    <div>
      <div>我是父组件???</div>
      <Child
        name={user.name}
        age={user.age}
        func={() => {
          console.log("我是父组件传递给子组件的函数");
        }}
      >
        <span>我写在Child组件的里面,是一个span元素</span>
      </Child>
    </div>
  );
}
export default Father;

永远记住一点:如果你想要将定义的数据的变化实时的反映到页面,那就用 useState 来定义状态,使用 setXxx 来设置状态。

在这个例子中给出了比较多的示例。首先,React 中的 Props 几乎能够传递 任意形式的数据

  1. 基本数据类型

    • 字符串(String)
    • 数字(Number)
    • 布尔值(Boolean)
    • 空值(null)
    • 未定义(undefined)
  2. 复合数据类型

    • 对象(Object):可以传递包含多种键值对的对象。
    • 数组(Array):可以传递一组值,这些值可以是任意类型。
  3. 函数

    • 可以将函数作为 props 传递, 这在父子组件通信时尤其有用,比如将父组件的函数传递给子组件来调用。
  4. React 元素和组件

    • React 元素:可以传递 JSX,即 React 元素,这允许你嵌套组件或在父组件中预先定义布局元素。
    • 组件:可以传递一个组件作为 props ,让接收的组件根据需要渲染或实例化传递的组件。
  5. Symbol

    • ECMAScript 的 Symbol 类型也可以通过 props 传递。
  6. 其他

    • 除了上述类型,React 的 props 还可以传递特定的 JavaScript 对象,如 PromiseMapSet 等,尽管这些用例较少见。

子组件接收父组件传递的数据时,是直接在子组件函数的参数中指定的。既可以直接用 props 来接,也可以先用 {} 将其解构后,单独的拿出传递的元素属性来。如果是使用 props 来接,那么需要用 props.元素名 来取出元素。

除了上述的举例之外,我们可以看到,这个示例当中在我们的 Child 组件的里面写了一个 span 元素,Child 标签不是自闭合了,而是和 html 元素一样的有开有闭,中间是我们写的 JSX 或者 html 元素。 这样写在子组件中间的 JSX/HTML 标签,在子组件可以通过 children 属性来接取。

# React 子传父 ——props 函数驱动

先回想一下 Vue 中是怎么处理子传父的,我们以 Vue3+TS+Setup 语法糖来分析:

  1. 先在子组件使用 defineEmits 来定义子传父需要触发的事件列表,并定义好传递的值的类型:

    const emits = defineEmits<{
      (e: "sendMsg", value: string): void;
    }>();
  2. 在子组件中想要触发这个事件的函数中引入这个方法并且传入参数:

    const send = (msg: string) => {
      emits("sendMsg", msg);
    };
  3. 在父组件的引入子组件的地方,用 @事件名 来接收事件,并再通过一个自定义的函数来接收传递的数据并做处理。这个自定义的函数不需要加任何的参数,Vue 会自动帮你传入:

    <template>
      <Child @sendMsg="getMsg" />
    </template>
    
    <script setup lang="ts">
    // ...
    const getMsg = (msg: string) => {
      console.log(msg);
    };
    </script>
    

再 React 中,虽然也是函数驱动,不过实现的相较于 Vue 用 Emits 额外封装了一层,显得更加纯粹,仅仅只是利用了 props 中可以传函数的这一个机制

import { useState } from "react";
function Child({ onSendMsg }) {
  return (
    <div>
      <div>我是子组件</div>
      <button onClick={() => onSendMsg("你好,父组件!")}>
        点击我,向父组件传递数据
      </button>
    </div>
  );
}
function Father() {
  const [msg, setMsg] = useState("");
  const sendMsg = (msg) => {
    console.log("父组件接收到的数据:", msg);
    setMsg(msg);
  };
  return (
    <div>
      <div>我是父组件</div>
      <Child onSendMsg={sendMsg} />
      <div>我接收到了子组件传递的数据:{msg}</div>
    </div>
  );
}
export default Father;

既然你可以传递函数,那么我就接啊,接了之后在我自己想要调用的地方进行调用,并且传递一个参数。

父组件只是把这个函数传给了子组件,子组件在调用的这个函数仍旧是发生在父组件中的!!

父组件的这个函数接收到子组件传的参数之后,就可以开始进行一系列的处理了。

# React 兄弟通信 —— 状态提升

状态提升,就是把兄弟之间的通信统一的放到他们的父组件中进行。这一点就是直接结合了子传父 + 父传子的这一个过程,而响应式数据的维护就直接放在父组件里面进行维护。

import { useState } from "react";
function Brother1({ onSendMsg }) {
  return (
    <div>
      <div>我是兄弟组件1</div>
      <button onClick={() => onSendMsg("你好,兄弟组件2!")}>
        点击我,向兄弟组件2传递数据
      </button>
    </div>
  );
}
function Brother2({ msg }) {
  return (
    <div>
      <div>我是兄弟组件2</div>
      <div>我接收到了兄弟组件1传递的数据:{msg}</div>
    </div>
  );
}
// 作为一个中间人,负责接收数据并传递给兄弟组件(状态提升)
function Parent() {
  const [msg, setMsg] = useState("");
  const sendMsg = (msg) => {
    console.log("兄弟组件1开始发送数据", msg);
    setMsg(msg);
  };
  return (
    <div>
      <div>我是父组件</div>
      <Brother1 onSendMsg={sendMsg} />
      <Brother2 msg={msg} />
    </div>
  );
}
export default Parent;

如果想要兄弟 1 传给兄弟 2,那么兄弟 1 需要接收函数来触发子传父;兄弟二需要接收值来处理。

# React 跨层级通信 ——Context

虽然本来想说 React 当中的 Context 类似于 Vue 中的事件总线(bus),但是转念一想又感觉不对,bus 侧重于事件的发射与接收,Context 只是单纯的数据传递。

Context 适用于无论嵌套多少层的数据传递,前提要传递数据的是必须是自己的子孙。以下给一个例子:

// 使用 Context 进行跨层级组件通信
import { useState } from "react";
import { createContext, useContext } from "react";
const MyContext = createContext(); // 上下文对象要在组件外部创建
function Son() {
  const ctx = useContext(MyContext); // 通过 useContext 获取 Provider 组件传递的数据,里面要传一个 context 对象
  return (
    <div>
      <div>我是儿子组件</div>
      <div>
        我接收到了爷爷组件传递的数据:
        {ctx}
      </div>
    </div>
  );
}
function Father() {
  return (
    <div>
      <div>我是爸爸组件</div>
      <Son />
    </div>
  );
}
function GrandFather() {
  const [msg, setMsg] = useState("");
  const sendMsg = () => {
    console.log("爷爷组件开始发送数据");
    setMsg("你好,孙子组件!");
  };
  return (
    // 通过 Provider 组件向子孙组件传递数据
    <MyContext.Provider value={msg}>
      <div>我是爷爷组件</div>
      <button onClick={() => sendMsg()}>点击我,向孙子组件发数据</button>
      <Father />
    </MyContext.Provider>
  );
}
export default GrandFather;

这个例子里面,总共有三代:Son、Father、GrandFather。现在想要祖父给孙子传数据,那么需要借助 Context。

  1. 引入 createContext 创建一个上下文对象。 注意:创建上下文对象必须要放在组件外侧!! 实际开发中是组件分离的,那么需要在创建好之后将其 export 导出,在孙子组件中 import 进来,以作为参数传给 useContext。
  2. 在祖父组件中定义好状态以及一些其他的函数等,用 MyContext.Provider 来包裹自己的子组件,使得数据在其中传递。
  3. 在孙子组件中使用 useContext 并且传入在第一步中定义好的上下文对象,左边用一个常量接住,这个常量就是祖父组件传给孙子组件的值。

# Redux—— 状态集中管理

详情请参考 Redux 一文。Redux 和 Pinia 属性比较像,是解决组件通信问题的究极大杀器。只不过 Redux 本身的使用有一些过于臃肿,可以考虑更为更接近 Pinia 使用的 Zustand。