前言
React通过组件化来处理复杂的页面。界面被拆成一个个可复用、可组合的独立组件,但组件间又需要共享信息和触发行为,这就涉及到组件间是如何通信的。我们知道通信是一种建立在关系上的行为,所以需要依据组件间的关系来选择组件间的通信方式。
一、父子关系
父子关系是最常见的结构,大多数界面都是通过父组件嵌套子组件形成的。
我们定义一个父组件与一个子组件。
父组件:
import Child from './components/child'
//创建一个函数型组件
function App() {
return (
<>
<h1>父组件</h1>
<Child/>
</>
)
}
export default App
子组件:
//创建一个函数型组件
const Child = () => {
return (
<>
<h2>子组件</h2>
</>
)
}
export default Child
1)父对子
直接通过props传递数据。
父组件:
import Child from './components/child'
import {useState} from 'react'
function App() {
//设置父组件的state
const [state, setState] = useState(
"我是父亲的state"
)
return (
<>
<h1>父组件</h1>
//以props的形式传递state
<Child state={state}/>
</>
)
}
export default App
子组件:
//接收父组件的state
const Child = ({state}) => {
return (
<>
<h2>子组件</h2>
//渲染父亲state
<div>{state}</div>
</>
)
}
export default Child
2)子对父
子组件可以通过父组件在props中传递的函数来修改父组件的state值。
父组件:
function App() {
const [state, setState] = useState(
"我是父亲的state"
)
return (
<>
<h1>父组件</h1>
//传递修改state的方法
<Child state={state} setState={setState} />
</>
)
}
export default App
子组件:
//接收修改state的方法
const Child = ({ state, setState }) => {
//修改state
useEffect(()=>{
setState("我是修改后的父亲state")
},[])
return (
<>
<h2>子组件</h2>
<div>{state}</div>
</>
)
}
export default Child
这里用了一个useEffect防止每一次渲染时都改变父组件的state。
二、兄弟关系(非嵌套组件)
兄弟关系中的两组件不具有直接的父子关系,我们并不能直接通过props传递数据。可以通过两种方式通信:
1)状态提升
状态提升:将兄弟组件各自的state提升到共同的父组件身上,再以props的形式传递回去。
设立情景:
我们创建了两个组件,分别是BrotherA与BrotherB。
BrotherA:
import { useEffect } from "react"
const BrotherA = ({}) => {
return (
<>
<h2>兄弟A</h2>
<button>我要修改兄弟B的state</button>
</>
)
}
export default BrotherA
BrotherB:
import {useState} from 'react'
const BrotherB = () => {
const [state,setState] = useState(
"我是兄弟B的state"
)
return (
<>
<h2>兄弟B</h2>
<div>{state}</div>
</>
)
}
export default BrotherB
此时的页面是这样的:
想要通过按下按钮触发BrotherB的state的改变,可以通过把B的state提升到父组件中,再把B的state的修改方法传给A。
此时的父组件:
import './App.css'
import BrotherA from './components/brotherA'
import BrotherB from './components/brotherB'
import { useState } from 'react'
function App() {
const [state,setState] = useState(
"我是兄弟B的state"
)
return (
<>
<h1>父组件</h1>
//把setState传递给A
<BrotherA setState={setState}/>
//把State传递给B
<BrotherB state={state}/>
</>
)
}
export default App
此时的A:
const BrotherA = ({setState}) => {
return (
<>
<h2>兄弟A</h2>
//修改B的state
<button onClick={()=>{setState('我修改好啦!')}}>我要修改兄弟B的state</button>
</>
)
}
export default BrotherA
我们来看实际效果。
在按下按钮前的页面:
按下按钮后的页面:
2)消息订阅-发送
我们也可以使用消息订阅-发送的方式来实现兄弟组件间的通信。
消息订阅-发送(PubSub):一个组件发布消息,一个组件订阅消息。两个组件间不需要父子关系。
首先需要安装一个常见的PubSub库"pubsub-js"(当然也有其他的库能实现消息订阅与发送):
yarn add pubsub-js
我们回到状态提升前的兄弟俩,并使用PubSub。
BrotherA:
const BrotherA = ({}) => {
const handleClick = () => {
PubSub.publish("UPDATE_B_STATE", "我修改好啦!");
};
return (
<>
<h2>兄弟A</h2>
<button onClick={handleClick}>我要修改兄弟B的state</button>
</>
)
}
BrotherB:
import PubSub from 'pubsub-js'
const BrotherB = ({}) => {
const [state,setState] = useState(
"我是兄弟B的state"
)
useEffect(() => {
// 订阅
const token = PubSub.subscribe("UPDATE_B_STATE", (msgName, data) => {
setState(data);
});
// 卸载时取消订阅
return () => PubSub.unsubscribe(token);
}, []);
return (
<>
<h2>兄弟B</h2>
<div>{state}</div>
</>
)
}
我们在BrotherA中定义了一个函数,在按下按钮时触发并发布一则消息。
BrotherB在组件挂载时订阅消息,持续监听消息的发布,当消息发布时捕获data并给setState()使用。
在组件卸载时取消订阅。
当然,对于兄弟关系的组件不仅仅可以使用状态提升与PubSub,还可以使用下面将要提到的如集中式管理(redux)等方式。
三、祖孙关系(跨级组件)
对于跨级组件我们也可以使用消息订阅-发布,同时也有Context、Redux等通信方式。
1)Context
Context是React内置的跨组件数据共享机制。当我们想要在跨级组件之间传递数据时,使用props去做一层层的传递比较麻烦。Context使我们可以将全局性质的数据放在一个地方,任意深度的组件都能直接访问。
下面是代码示例:
(1)创建Context(theme.js):
import { createContext } from "react";
export const ThemeContext = createContext();
ThemeContext中包含两个东西:
- Provider(提供数据)
- Consumer(消费数据)
(2)用Provider包裹组件树,并提供value属性:
//引入Context
import { ThemeContext } from "./theme";
const App = () => {
return (
<ThemeContext.Provider value={{ theme: 'dark' }}>
<Layout />
</ThemeContext.Provider>
);
}
这里用value给后代组件传递一个theme属性。
(3)在子组件中获取数据:
//函数组件
import { useContext } from "react";
import { ThemeContext } from "./theme";
const Header = () => {
const { theme } = useContext(ThemeContext);
return <h1 className={theme}>标题</h1>;
};
除了使用useContext(常用)之外,子组件还有两种获取数据的方式:
- 仅适用于类组件:
class Header extends React.Component {
static contextType = ThemeContext; // 声明
render() {
// 读取
const { theme } = this.context;
return <h1>当前主题:{theme}</h1>;
}
}
- 函数组件与类组件都可以:
//使用Consumer
<ThemeContext.Consumer>
{
value => (
<div>当前主题:{value.theme}</div>
)
}
</ThemeContext.Consumer>
2)Redux
Redux是什么?
Redux是一个帮助管理“全局状态”的框架。
它能做什么?
把所有全局状态都放到一个中央仓库中,所有组件都统一遵守流程:
dispach -> action -> reducer -> 新state
Redux有三个核心:
- state(全局状态):放在仓库(store)中。
- action(要做什么):
它是一个类似于这样的普通对象:{ type: "add", payload: 3 } - reducer:一个纯函数,负责修改state。
我们来写一个简单的计算器案例。
首先在根目录下创建一个redux文件夹,其中包含四个文件:
store.js:
/*
该文件专门用于暴露一个store对象,整个应用只有一个store对象
*/
//引入createStore,专门用于创建redux中最为核心的store对象
import {createStore} from 'redux'
//引入为Count组件服务的reducer
import countReducer from './count_reducer'
//暴露store
export default createStore(countReducer)
count_action.js:
/*
该文件专门为Count组件生成action对象
*/
import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => ({type:INCREMENT,data})
export const createDecrementAction = data => ({type:DECREMENT,data})
同时我们创建了一个文件constant.js用于定义action对象中type类型的常量值:
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
我们在这里创建了两个行为:加法和减法。通过下面的reducer的代码我们可以看到reducer可以根据我们选择的
action来对state做出相应的操作。
count_reducer.js:
/*
1.该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
2.reducer函数会接到两个参数,分别为:之前的状态(preState),动作对象(action)
*/
import {INCREMENT,DECREMENT} from './constant'
const initState = 0 //初始化状态
export default function countReducer(preState=initState,action){
// console.log(preState);
//从action对象中获取:type、data
const {type,data} = action
//根据type决定如何加工数据
switch (type) {
case INCREMENT: //如果是加
return preState + data
case DECREMENT: //如果是减
return preState - data
default:
return preState
}
}
来到组件中,我们创建了这样的计算器:
import React, { Component } from 'react'
//引入store,用于获取redux中保存状态
import store from '../../redux/store'
//引入actionCreator,专门用于创建action对象
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
export default class Count extends Component {
state = {carName:'奔驰c63'}
//加法
increment = ()=>{
const {value} = this.selectNumber
store.dispatch(createIncrementAction(value*1))
}
//减法
decrement = ()=>{
const {value} = this.selectNumber
store.dispatch(createDecrementAction(value*1))
}
//奇数再加
incrementIfOdd = ()=>{
const {value} = this.selectNumber
const count = store.getState()
if(count % 2 !== 0){
store.dispatch(createIncrementAction(value*1))
}
}
//异步加
incrementAsync = ()=>{
const {value} = this.selectNumber
setTimeout(()=>{
store.dispatch(createIncrementAction(value*1))
},500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={c => this.selectNumber = c}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button onClick={this.increment}>+</button>
<button onClick={this.decrement}>-</button>
<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}
通过灵活使用redux中的相同action,我们可以实现对state的不同操作。
3)Context与Redux的对比
| 特性 | Context | Redux |
|---|---|---|
| 本质 | 用来避免props逐层传播 | 全局复杂状态管理工具 |
| 管理逻辑 | 没有统一逻辑,只是共享数据 | 有严格的 action → reducer → state 流程 |
| 数据存储 | 很轻 | 规范的中央仓库store |
| 适用场景 | 轻量、简单、不经常变的全局数据 | 复杂、频繁变化、共享的大量状态 |
我们通常在小项目中使用Context,在中大型项目使用Redux。
小结
在组件间通信的场景中,我们可以依据通信组件间的关系来采取相对应更合适的通信方式。
以上是小编在学习过程中的浅薄分享,期待在具体项目实践中有更深的认识。