React中组件间的通信方式

78 阅读7分钟

前言

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

image.png

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。

image.png

二、兄弟关系(非嵌套组件)

兄弟关系中的两组件不具有直接的父子关系,我们并不能直接通过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

此时的页面是这样的:

image.png

想要通过按下按钮触发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

我们来看实际效果。
在按下按钮前的页面:

image.png
按下按钮后的页面:

image.png

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(常用)之外,子组件还有两种获取数据的方式:

  1. 仅适用于类组件:
class Header extends React.Component {
  static contextType = ThemeContext; // 声明

  render() {
    // 读取
    const { theme } = this.context;

    return <h1>当前主题:{theme}</h1>;
  }
}
  1. 函数组件与类组件都可以:
//使用Consumer
<ThemeContext.Consumer>
  {
    value => (
      <div>当前主题:{value.theme}</div>
    )
  }
</ThemeContext.Consumer>

2)Redux

Redux是什么?

Redux是一个帮助管理“全局状态”的框架。

它能做什么?

把所有全局状态都放到一个中央仓库中,所有组件都统一遵守流程:
dispach -> action -> reducer -> 新state

image.png

Redux有三个核心:

  1. state(全局状态):放在仓库(store)中。
  2. action(要做什么):
    它是一个类似于这样的普通对象: { type: "add", payload: 3 }
  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>&nbsp;
				<button onClick={this.increment}>+</button>&nbsp;
				<button onClick={this.decrement}>-</button>&nbsp;
				<button onClick={this.incrementIfOdd}>当前求和为奇数再加</button>&nbsp;
				<button onClick={this.incrementAsync}>异步加</button>&nbsp;
			</div>
		)
	}
}

image.png
通过灵活使用redux中的相同action,我们可以实现对state的不同操作。

3)Context与Redux的对比

特性ContextRedux
本质用来避免props逐层传播全局复杂状态管理工具
管理逻辑没有统一逻辑,只是共享数据有严格的 action → reducer → state 流程
数据存储很轻规范的中央仓库store
适用场景轻量、简单、不经常变的全局数据复杂、频繁变化、共享的大量状态

我们通常在小项目中使用Context,在中大型项目使用Redux。

小结

在组件间通信的场景中,我们可以依据通信组件间的关系来采取相对应更合适的通信方式。
以上是小编在学习过程中的浅薄分享,期待在具体项目实践中有更深的认识。