# 组件间通信
无论是 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 几乎能够传递 任意形式的数据 :
-
基本数据类型:
- 字符串(String)
- 数字(Number)
- 布尔值(Boolean)
- 空值(null)
- 未定义(undefined)
-
复合数据类型:
- 对象(Object):可以传递包含多种键值对的对象。
- 数组(Array):可以传递一组值,这些值可以是任意类型。
-
函数:
- 可以将函数作为
props
传递, 这在父子组件通信时尤其有用,比如将父组件的函数传递给子组件来调用。
- 可以将函数作为
-
React 元素和组件:
- React 元素:可以传递 JSX,即 React 元素,这允许你嵌套组件或在父组件中预先定义布局元素。
- 组件:可以传递一个组件作为
props
,让接收的组件根据需要渲染或实例化传递的组件。
-
Symbol:
- ECMAScript 的 Symbol 类型也可以通过
props
传递。
- ECMAScript 的 Symbol 类型也可以通过
-
其他:
- 除了上述类型,React 的
props
还可以传递特定的 JavaScript 对象,如Promise
、Map
、Set
等,尽管这些用例较少见。
- 除了上述类型,React 的
子组件接收父组件传递的数据时,是直接在子组件函数的参数中指定的。既可以直接用 props 来接,也可以先用 {} 将其解构后,单独的拿出传递的元素属性来。如果是使用 props 来接,那么需要用 props.元素名
来取出元素。
除了上述的举例之外,我们可以看到,这个示例当中在我们的 Child 组件的里面写了一个 span 元素,Child 标签不是自闭合了,而是和 html 元素一样的有开有闭,中间是我们写的 JSX 或者 html 元素。 这样写在子组件中间的 JSX/HTML 标签,在子组件可以通过 children 属性来接取。
# React 子传父 ——props 函数驱动
先回想一下 Vue 中是怎么处理子传父的,我们以 Vue3+TS+Setup 语法糖来分析:
-
先在子组件使用
defineEmits
来定义子传父需要触发的事件列表,并定义好传递的值的类型:const emits = defineEmits<{
(e: "sendMsg", value: string): void;
}>();
-
在子组件中想要触发这个事件的函数中引入这个方法并且传入参数:
const send = (msg: string) => {
emits("sendMsg", msg);
};
-
在父组件的引入子组件的地方,用
@事件名
来接收事件,并再通过一个自定义的函数来接收传递的数据并做处理。这个自定义的函数不需要加任何的参数,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。
- 引入
createContext
创建一个上下文对象。 注意:创建上下文对象必须要放在组件外侧!! 实际开发中是组件分离的,那么需要在创建好之后将其 export 导出,在孙子组件中 import 进来,以作为参数传给 useContext。 - 在祖父组件中定义好状态以及一些其他的函数等,用
MyContext.Provider
来包裹自己的子组件,使得数据在其中传递。 - 在孙子组件中使用
useContext
并且传入在第一步中定义好的上下文对象,左边用一个常量接住,这个常量就是祖父组件传给孙子组件的值。
# Redux—— 状态集中管理
详情请参考 Redux 一文。Redux 和 Pinia 属性比较像,是解决组件通信问题的究极大杀器。只不过 Redux 本身的使用有一些过于臃肿,可以考虑更为更接近 Pinia 使用的 Zustand。