从Vue2.0到React17——React组件之间的通讯

2,596 阅读6分钟

“这是我参与更文挑战的第5天,活动详情查看: 更文挑战

前言

React组件之间的关系可以分为父子关系,跨级关系,非嵌套关系。本文主要介绍一下各种关系的React组件之间是如何通讯的。

1、父子关系的React组件的通讯

父子关系的React组件的通讯,包括父组件给子组件传递数据和子组件改变父组件的数据两种通讯。

  • 将父组件的state赋值给子组件的props实现父组件向子组件的通讯。
  • 将父组件中改变自己数据的函数赋值给子组件的props,在子组件中调用该函数实现子组件向父组件的通讯

用一个例子来介绍,在HelloWorld组件中把父组件中的infostate展示出来,并且点击HelloWorld组件会改变父组件中的infostate。

  • 类组件的写法:
import React from 'react';
class HelloWorld extends React.Component {
  constructor(props) {
    super(props);
    this.handleChangeTitle=this.handleChangeTitle.bind(this);
  }
  handleChangeTitle(){
    this.props.changeTitle('hello React');
  }
  render() {
    return (
      <div>
        {this.props.title}
        <button 
          onClick={this.handleChangeTitle.bind(this)}>
          改变标题
        </button>
      </div>
    );
  }
}
HelloWorld.defaultProps = {
  title: 'hello world'
};
export default HelloWorld;
import HelloWorld from './HelloWorld.js'
import React from 'react'
class Index extends React.Component {
  constructor(props) {
    super(props);
    this.state={
      info:'hello world'
    };
    this.handleChangeTitle=this.handleChangeTitle.bind(this);
  }
  handleChangeTitle(data){
    this.setState(state =>{
      return {
        info:data
      }
    })
  }
  render() {
    return (
      <HelloWorld 
        title={this.state.info} 
        changeTitle={this.handleChangeTitle}>
      </HelloWorld>
    );
  }
}
export default Index;
  • 函数组件的写法:
export default function HelloWorld(props: any) {
  const { title = 'hello world', changeTitle } = props;
  const handleChangeTitle = () => {
    changeTitle('hello React')
  }
  return (
    <div>
      {title}
      <button onClick={handleChangeTitle}>改变标题</button>
    </div>
  );
}
import { useState } from 'react'
import HelloWorld from './HelloWorld.js'

export default function Index(){
  const [info,setInfo] = useState('hello world');
  const handleChangeTitle = (data)=>{
    setInfo(data);
  }
  return (
    <HelloWorld title={info} changeTitle={handleChangeTitle}/>
  )
}

将父组件的infostate赋值给子组件的titleprop,完成父组件向子组件的通讯,然后子组件中把titleprop展示出来,即把父组件的infostate展示出来。

在父组件中一个可以改变infostate的handleChangeTitle函数,并把该函数传递给子组件的changeTitleprop,在子组件中执行changeTitle即执行父组件中的handleChangeTitle函数,然后会改变infostate,完成子组件向父组件的通讯。

2、跨级关系的React组件的通讯

跨级关系的组件,比如祖先组件和后代组件之间的关系就是跨级关系,跨级关系的React组件不能像父子关系的React组件之间那样来通讯。

比如祖父组件要给孙子组件传递一个数据,总不能在组件之间一层一层的传递props,多么繁琐。

在Vue使用provide在祖先组件中注入一些数据,在后代组件中用inject来接受这些数据,称做依赖注入。

而在React也有类似的功能,叫做Context。

Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。

先用React.createContext创建一个Context,然后用Context.Provider给祖先组件注入一些数据。然后在后代组件中用Class.contextType或者Context.Consumer来获取这些数据。

  • 类组件的写法

先在context.js文件中创建一个Context。

import React from 'react';
export const MyContext = React.createContext(
  //在这里可以设置一些默认值
  {title:'hello world'}
);

在祖先组件Grandfather中从context.js引入MyContext,然后用MyContext.Provider包裹子组件Father,并把infostate赋值给MyContext.Provider上的属性value,相当于注入数据。

import React from 'react';
import Father from './Father.js';
import { MyContext } from './context.js';
export default class Grandfather extends React.Component {
  constructor(){
    super();
    this.state={
      info:{
        title:'hello react'
      }
    }
  }
  render() {
    return (
      <MyContext.Provider value={this.state.info}>
        <Father>
        </Father>
      </MyContext.Provider>
    )
  }
}

在Father组件中引入子组件Son。

import React from 'react';
import Son from './Son.js';
export default class Father extends React.Component{
  render(){
    return(
      <Son></Son>
    )
  }
}

此时,组件Son就是组件Grandfather的后代组件。可以看到组件Father并没有通过props从祖先组件Grandfather中获取数据info,再通过props传递给组件Son,那么在组件Son中怎么才能获取到数据info呢?

在类组件中,先把MyContext赋值到类组件中的静态属性contextType,然后用this.context来获取该组件的上级组件中最近一个被MyContext.Provider包裹的组件中传递给MyContext.Provider的属性value的数据。

import React from 'react';
import { MyContext } from './context.js';
export default class Son extends React.Component {
  static contextType = MyContext;
  render() {
    return (
      <div>
        {this.context.title}
      </div>
    )
  }
}

在函数组件中,用MyContext.Consumer包裹一个函数式组件,该函数式组件的参数,可以接收该组件的上级组件中最近一个被MyContext.Provider包裹的组件中传递给MyContext.Provider的属性value的数据。

import React from 'react';
import { MyContext } from './context.js';
export default class Son extends React.Component {
  static contextType = MyContext;
  render() {
    return (
      <MyContext.Consumer>
        {value => (<div>{value.title}</div>)}
      </MyContext.Consumer>
    )
  }
}

还记得用React.createContext创建Context,可以接受一些数据,作为默认数据。如果使用Class.contextType或者Context.Consumer的组件的上级组件中没有一个被MyContext.Provider包裹,那么this.contextvalue可以获取到这些默认数据。

3、非嵌套关系的React组件的通讯

非嵌套关系的组件,比如兄弟组件之间的关系就是非嵌套关系。可以给兄弟组件创建一个共同父组件,用父组件作为一个中转站,来处理兄弟组件的之间的通讯。

例如,组件A和组件B是两个兄弟组件,创建一个共同父组件C,把组件A和组件B要通讯的state提取到父组件C的state中,再赋值给组件A和组件B的props,传递到组件A和组件B中。然后在父组件C中定义一个修改state的函数,再把函数赋值给组件A和组件B的props。那么在组件A或组件B调用这个函数的prop,改变父组件C的state,在通过props把改变后的state传递给组件A或组件B,即完成了兄弟组件的通讯。

例如:要组件A中的num1state和组件B中的num2state进行通讯,改变组件A中的num1state,组件B中的num2state也会改变。下面来实现一下:

组件A:

import React,{useState} from 'react';
const A = () =>{
  const [num1,setNum1] = useState(1)
  return (
    <div>{num1}</div>
  )
}
export default A;

组件B:

import React,{useState} from 'react';
const B = () =>{
  const [num2,setNum2] = useState(2)
  return (
    <div>{num2}</div>
  )
}
export default B;

先创建一个父组件C把组件A和组件B包裹进去,把组件A的num1state和组件B的num2提取到组件C中,用numstate存储,再把numstate通过props传递给组件A和组件B。同时定义一个能改变num的函数,通过props传递传递给组件A和组件B,再组件A和组件B执行该函数,即可以实现组件A和组件B的通讯。

组件C

import React,{useState} from 'react';
import A from './A';
import B from './B';
const C = () =>{
  const [num,setNum] = useState(1)
  const handleChangNum = (data)=>{
     setNum(data)
  }
  return (
    <div>
      <A num1={num} changNum={handleChangNum}></A>
      <B num2={num} changNum={handleChangNum}></B>
    </div>
  )
}
export default C;

组件A

import React from 'react';
const A = (props) =>{
  const {num1,changNum} = props;
  const handleChang = () =>{
     //改变组件B中的num2
     changNum(2);
  }
  return (
    <div onClick={handleChang}>{num1}</div>
  )
}
export default A;

组件B

import React from 'react';
const B = (props) =>{
  const {num2,changNum} = props;
  const handleChang = () =>{
     //改变组件A中的num1
     changNum(2);
  }
  return (
    <div onClick={handleChang}>{num1}</div>
  )
}
export default B;

4、使用Events Bus来进行通讯

回顾Vue中用vm.$onvm.$offvm.$emit进行组件通讯,其原理就是Events Bus。在React中没有提供类似的API,我们可以利用node.js提供的events.js来实现,其文档可以点这里

首先执行npm install events --save下载events.js。再创建一个eventbus.js,在里面引入events.js,并创建一个EventEmitter实例。

import { EventEmitter } from "events";
export default new EventEmitter();

EventEmitter实例提供了类似vm.$onvm.$offvm.$emit的实例方法,

  • emitter.on(eventName, listener)对应vm.$on( event, callback )
  • emitter.off(eventName, listener)对应vm.$off( [event, callback] )
  • emitter.emit(eventName[, ...args])对应vm.$emit( eventName, […args] )

其中eventName是要监听事件的名称,listener被监听事件的回调函数,...args传递给监听事件的回调的参数。

那么,例如两个非嵌套关系的组件A和组件B,也可以通过events.js来实现通讯,可以在组件A的挂载阶段用emitter.on绑定要监听事件,在组件的卸载阶段用emitter.off解绑要监听事件,在组件B用emitter.emit触发被监听事件,把emitter.emit第二参数开始的参数,当作参数传递给emitter.on绑定的监听事件的回调函数,即实现组件A中接收到组件B向组件A发起通讯传递数据的功能。

函数组件的实现:

组件A

import React,{useEffect} from 'react';
import emitter from "./ev"
const A = () =>{
  const handleChange = (param1,param2)=>{
    console.log(param1);
    console.log(param2);
  }
  useEffect(() =>{
    emitter.on('chang',handleChange);
    return () =>{
      emitter.off('chang',handleChange);
    }
  },[])
  return(
    <div>A组件</div>
  )
}
export default A;

组件B

import React from 'react';
import emitter from "./ev"
const B = () =>{
  const handleChange = () =>{
    emitter.emit('chang','参数1','参数2')
  }
  return(
    <div onClick={handleChange}>B组件</div>
  )
}
export default B;

此时点击组件B,控制台会打印出“参数1”和“参数2”。