什么是兄弟组件通信

所谓兄弟组件通讯就是非父子关系组件的通讯,所有的非父子关系组件通讯都可以称为兄弟组件通讯,在 Vue 中,我们的兄弟组件通信方式会选择使用 eventbus 作为中间人来通讯(其实就是订阅者模式),那么 React 怎么实现兄弟组件通信呢?具体有哪些方案呢?


方法一 – 状态提升 :

状态提升就是将多个组件需要共享的状态提升到他们的最近的父组件,由父组件改变这个状态并通过 props 属性传递给子组件

React -三种数据通信方法都怎么用?什么时候用?-LMLPHP

我们从图中不难看出这种兄弟组件的通讯方式依旧还是基于父子通信理念的,并且如果子组件的子组件相互进行通讯的话,代码会变得难以阅读以及繁琐,俩个组件传递一个信息竟然前前后后传了 4 遍,所以这种方法我们可以使用亲兄弟之间的数据通信,其他的还是算了

我们来举一个亲兄弟通信的例子:

  • JSON 文件模拟服务端数据
{
  "state": 0,
  "data": [
    {
      "id": 0,
      "user_name": "Brave-AirPig",
      "age": 22,
      "hobby": ["打代码", "玩游戏", "睡觉", "看书"],
      "say": "[00000000000000000000]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 1,
      "user_name": "Anna",
      "age": 18,
      "hobby": ["打代码", "看书"],
      "say": "[1111111111111111111]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 2,
      "user_name": "Bun",
      "age": 10,
      "hobby": ["玩游戏", "睡觉"],
      "say": "[222222222222222222]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 3,
      "user_name": "Jons",
      "age": 3,
      "hobby": ["看书"],
      "say": "[3333333333333]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 4,
      "user_name": "Tomy",
      "age": 34,
      "hobby": ["打代码", "玩游戏", "看书"],
      "say": "[44444444444444444444]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 5,
      "user_name": "Arisa",
      "age": 56,
      "hobby": ["打代码", "睡觉", "看书"],
      "say": "[55555555555555555]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 6,
      "user_name": "YoYo",
      "age": 28,
      "hobby": ["玩游戏", "睡觉", "看书"],
      "say": "[66666666666666666]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 7,
      "user_name": "BoBo",
      "age": 35,
      "hobby": ["睡觉"],
      "say": "[777777777777777777777777]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 8,
      "user_name": "PoPo",
      "age": 66,
      "hobby": ["打代码", "看书"],
      "say": "[8888888888888888888888]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    },
    {
      "id": 9,
      "user_name": "AvA",
      "age": 75,
      "hobby": ["打代码", "玩游戏"],
      "say": "[999999999999999999999]Lorem ipsum dolor sit amet consectetur adipisicing elit. Adipisci accusamus fugit aspernatur officiis beatae, debitis illum, aliquid ducimus blanditiis repellendus error alias! Eligendi, maxime. Ea laborum natus quisquam neque similique!"
    }
  ],
  "msg": "OK"
}
  • 父组件
import React, { Component } from 'react';
import axios from 'axios';

import User from './中间人模式/User';
import Message from './message';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      userList: [],
      say: '留言板',
    };
    axios.get('http://localhost:3000/test.json').then(res => {
      this.setState({
        userList: res.data.data,
      });
    });
  }

  render() {
    return (
      <section>
        {this.state.userList.map(list => (
          // 给到子组件(受控组件)key值以及父组件中请求回来的数据
          <User
            key={list.id}
            {...list}
            onEvent={say => {
              this.setState({
                say,
              });
            }}
          ></User>
        ))}
        <Message text={this.state.say}></Message>
      </section>
    );
  }
}
  • 用户信息子组件
import React, { Component } from 'react';

export default class App extends Component {
  render() {
    const { user_name, age, hobby, say } = this.props;

    return (
      <section
        style={{
          backgroundColor: '#E34053',
          padding: '24px 48px',
          margin: '12px 0',
          borderRadius: '15px',
          color: '#efefef',
        }}
        onClick={() => this.props.onEvent(say)}
      >
        <h3>用户名:{user_name}</h3>
        <span>年龄:{age}</span>

        <ul>
          {hobby.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </section>
    );
  }
}
  • 留言板子组件
import React, { Component } from 'react';

export default class App extends Component {
  render() {
    return (
      <section
        style={{
          backgroundColor: '#EAEAD4',
          position: 'fixed',
          top: '200px',
          right: '10px',
          width: '50vw',
          height: '50vh',
          borderRadius: '20px',
          fontSize: '30px',
          fontWeight: 'bold',
          padding: '24px 48px',
          textAlign: 'center',
        }}
      >
        {this.props.text}
      </section>
    );
  }
}

很容易理解,用户信息组件与留言板组件是亲兄弟组件,在案例中,我们由父组件来从后端请求数据,父传子给到用户信息模块,留言板给到留言板模块,可是在这里出现了一个问题,留言板需求是只要一个,点击信息模块对应的用户显示该用户对应的留言,这不就用到我们的兄弟通信了嘛 🤔,而且还是亲兄弟,正好我使用高级版父子通讯(状态提升)来解决

思路:点击信息模块中的用户模块,将用户留言通过回调函数传递给父组件,父组件改变自身状态,而留言板模块将基于 props 属性使用该父组件状态

React -三种数据通信方法都怎么用?什么时候用?-LMLPHP

方法二 – 发布订阅模式

React -三种数据通信方法都怎么用?什么时候用?-LMLPHP

根据这个思想我们来手写一个简单的发布订阅模式:

const EventBus = {
  // 管理者管理数据
  message: [],

  // 订阅
  subscribe(callback) {
    this.message.push(callback);
  },

  // 发布
  publish(text) {
    // 执行回调
    this.message.forEach(callback => callback && callback(text));
  },
};

// 订阅者
EventBus.subscribe(value => console.log(`${value}订阅【1】`));

// 发布者
setTimeout(() => EventBus.publish('数据:'), 0);

// 数据:订阅【1】

或者我们可以使用 Redux 库来实现复杂的数据通信,不过它也是基于订阅发布者思想来处理通信的

接下来我们对于上面已经写好的 状态提升 兄弟组件通信使用订阅发布思想重构一下:

  • EventBus 封装
const EventBus = {
  // 管理者管理数据
  message: [],

  // 订阅
  subscribe(callback) {
    this.message.push(callback);
  },

  // 发布
  publish(text) {
    // 执行回调
    this.message.forEach(callback => callback && callback(text));
  },
};

export default EventBus;
  • 父组件(既然这次不需要父组件来做中间人了,那么我们删除多余代码)
import React, { Component } from 'react';
import axios from 'axios';

import User from './中间人模式/User';
import Message from './message';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      userList: [],
    };
    axios.get('http://localhost:3000/test.json').then(res => {
      this.setState({
        userList: res.data.data,
      });
    });
  }

  render() {
    return (
      <section>
        {this.state.userList.map(list => (
          <User key={list.id} {...list}></User>
        ))}
        <Message></Message>
      </section>
    );
  }
}
  • 用户信息组件(作为发布者)
import React, { Component } from 'react';

import EventBus from '../EventBus';

export default class App extends Component {
  render() {
    const { user_name, age, hobby, say } = this.props;

    return (
      <section
        style={{
          backgroundColor: '#E34053',
          padding: '24px 48px',
          margin: '12px 0',
          borderRadius: '15px',
          color: '#efefef',
        }}
        // 发布者
        onClick={() => EventBus.publish(say)}
      >
        <h3>用户名:{user_name}</h3>
        <span>年龄:{age}</span>

        <ul>
          {hobby.map(item => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </section>
    );
  }
}
  • 留言板组件(作为订阅者)
import React, { Component } from 'react';

import EventBus from './EventBus';

export default class App extends Component {
  // 订阅
  constructor() {
    super();

    this.state = {
      say: '留言板',
    };

    EventBus.subscribe(say => {
      this.setState({
        say,
      });
    });
  }

  render() {
    return (
      <section
        style={{
          backgroundColor: '#EAEAD4',
          position: 'fixed',
          top: '200px',
          right: '10px',
          width: '50vw',
          height: '50vh',
          borderRadius: '20px',
          fontSize: '30px',
          fontWeight: 'bold',
          padding: '24px 48px',
          textAlign: 'center',
        }}
      >
        {this.state.say}
      </section>
    );
  }
}

COOL!! 😁 效果是一样的,我就不多做图示了


方法三 – context 状态数传参

React -三种数据通信方法都怎么用?什么时候用?-LMLPHP

这个貌似比订阅者要好理解一些 😀,并且 React 已经为我们封装好了

  • 共享 Context
import React from 'react';
// 创建 Context 上下文
const GlobalContext = React.createContext();
export default GlobalContext;
  • 父组件(供应商)
import React, { Component } from 'react';
import axios from 'axios';

import User from './中间人模式/User';
import Message from './message';

import GlobalContext from './globalContext';

export default class App extends Component {
  constructor() {
    super();
    this.state = {
      userList: [],
    };
    axios.get('http://localhost:3000/test.json').then(res => {
      this.setState({
        userList: res.data.data,
        message: '留言板',
      });
    });
  }

  render() {
    return (
      // 使用供应商组件包裹允许相互通信的组件(并通过 value 值传给消费者服务)
      <GlobalContext.Provider
        value={{
          message: this.state.message,
          // 后续消费者使用该回调将改变供应商中的状态
          // 只要供应商的状态改变了,那么必然会照成组件的重新渲染
          changeMessage: value => {
            this.setState({
              message: value,
            });
          },
        }}
      >
        <section>
          {this.state.userList.map(list => (
            <User key={list.id} {...list}></User>
          ))}
          <Message></Message>
        </section>
      </GlobalContext.Provider>
    );
  }
}
  • 用户信息子组件(消费者)
import React, { Component } from 'react';

import GlobalContext from '../globalContext';

export default class App extends Component {
  render() {
    const { user_name, age, hobby, say } = this.props;

    return (
      // 以 GlobalContext.Consumer 包裹的回调函数中返回结构代码(通过回调函数接收共享服务)
      <GlobalContext.Consumer>
        {message => {
          return (
            <section
              style={{
                backgroundColor: '#E34053',
                padding: '24px 48px',
                margin: '12px 0',
                borderRadius: '15px',
                color: '#efefef',
              }}
              // 将消费者中的数据传递给供应商
              onClick={() => message.changeMessage(say)}
            >
              <h3>用户名:{user_name}</h3>
              <span>年龄:{age}</span>

              <ul>
                {hobby.map(item => (
                  <li key={item}>{item}</li>
                ))}
              </ul>
            </section>
          );
        }}
      </GlobalContext.Consumer>
    );
  }
}
  • 留言板子组件(消费者)
import React, { Component } from 'react';

import GlobalContext from './globalContext';

export default class App extends Component {
  render() {
    return (
      <GlobalContext.Consumer>
        {message => {
          return (
            <section
              style={{
                backgroundColor: '#EAEAD4',
                position: 'fixed',
                top: '200px',
                right: '10px',
                width: '50vw',
                height: '50vh',
                borderRadius: '20px',
                fontSize: '30px',
                fontWeight: 'bold',
                padding: '24px 48px',
                textAlign: 'center',
              }}
            >
              {message.message}
            </section>
          );
        }}
      </GlobalContext.Consumer>
    );
  }
}

效果依旧和最初的一样,我就不过多的做展示了

  • 总结:创建一个供应商组件 React.createContext(), 给需要通信的子组件所在的父组件上包裹一个 context.Provider 供应商组件,供应商提供 value 数据通信服务,主要难以理解的点在于数据的更改怎么才能导致重新组件的渲染

我们到底该什么时候用什么方法

具体还是要根据实际情况来决定,如果简单的亲兄弟之间传递数据,我觉得是没有必要额外增加组件的(如 EventBusContext),如果项目较大,子组件繁多并且业务通讯非常频繁,那么使用 Context状态树 我个人觉得更为合适,其实 EventBus 也由用武之地,这是一种非常不错的思想

以上是我个人见解 – 如有不同见解或者看法大家可以评论或者私信我交流

08-20 14:25