10种React组件之间通信的方法

2023-11-15

组件间通信方式总结

父组件 => 子组件:

        1、Props
        2、Instance Methods - refs

子组件 => 父组件:

        3、Callback Functions - 回调函数

        4、Event Bubbling - 事件冒泡机制

兄弟组件之间:

        5、Parent Component - 利用父组件通信

不太相关的组件之间:

        6、Context

        7、Portals - 适用 Tooltip、Modal、Popup等

        8、Global Variables - 全局变量

        9、Observer Pattern - 观察者模式

        10、Redux/mobx等

1、Props

这是最常见的react组件之间传递信息的方法了吧,父组件通过props把数据传给子组件,子组件通过this.props去使用相应的数据。

const Child = ({ name }) => {    <div>{name}</div>}class Parent extends React.Component {    constructor(props) {        super(props)        this.state = {            name: 'zach'        }    }    render() {        return (            <Child name={this.state.name} />        )    }}

2、Instance Methods

第二种父组件向子组件传递信息的方式有些同学可能会比较陌生,但这种方式非常有用,请务必掌握。原理就是:父组件可以通过使用refs来直接调用子组件实例的方法,看下面的例子:

class Child extends React.Component {  myFunc() {    return "hello"  }}class Parent extends React.Component {  componentDidMount() {    var x = this.foo.myFunc()   // x is now 'hello'  }  render() {    return (      <Child        ref={foo => {          this.foo = foo        }}      />    )  }}

大致的过程:

  1. 首先子组件有一个方法myFunc。

  2. 父组件给子组件传递一个ref属性,并且采用callback-refs的形式。这个callback函数接收react组件实例/原生dom元素作为它的参数。当父组件挂载时,react会去执行这个ref回调函数,并将子组件实例作为参数传给回调函数,然后我们把子组件实例赋值给this.foo。

  3. 最后我们在父组件当中就可以使用this.foo来调用子组件的方法咯

了解了这个方法的原理后,我们要考虑的问题就是为啥我们要用这种方法,它的使用场景是什么?最常见的一种使用场景:比如子组件是一个modal弹窗组件,子组件里有显示/隐藏这个modal弹窗的各种方法,我们就可以通过使用这个方法,直接在父组件上调用子组件实例的这些方法来操控子组件的显示/隐藏。

这种方法比起你传递一个控制modal显示/隐藏的props给子组件要美观多了。

class Modal extends React.Component {  show = () => {// do something to show the modal}  hide = () => {// do something to hide the modal}  render() {    return <div>I'm a modal</div>  }}class Parent extends React.Component {  componentDidMount() {    if(// some condition) {        this.modal.show()    }  }  render() {    return (      <Modal        ref={el => {          this.modal = el        }}      />    )  }}

3、Callback Functions

讲完了父组件给子组件传递信息的两种方式,我们再来讲子组件给父组件传递信息的方法。

回调函数这个方法也是react最常见的一种方式,子组件通过调用父组件传来的回调函数,从而将数据传给父组件。

const Child = ({ onClick }) => {    <div onClick={() => onClick('zach')}>Click Me</div>}class Parent extends React.Component {    handleClick = (data) => {        console.log("Parent received value from child: " + data)    }    render() {        return (            <Child onClick={this.handleClick} />        )    }}

4、 Event Bubbling

这种方法其实跟react本身没有关系,我们利用的是原生dom元素的事件冒泡机制。

class Parent extends React.Component {  render() {    return (      <div onClick={this.handleClick}>         <Child />      </div>    );  }  handleClick = () => {    console.log('clicked')  }}function Child {  return (    <button>Click</button>  );    }

巧妙的利用下事件冒泡机制,我们就可以很方便的在父组件的元素上接收到来自子组件元素的点击事件

5、Parent Component

讲完了父子组件间的通信,再来看非父子组件之间的通信方法。一般来说,两个非父子组件想要通信,首先我们可以看看它们是否是兄弟组件,即它们是否在同一个父组件下。

如果不是的话,考虑下用一个组件把它们包裹起来从而变成兄弟组件是否合适。这样一来,它们就可以通过父组件作为中间层来实现数据互通了。

class Parent extends React.Component {  constructor(props) {    super(props)    this.state = {count: 0}  }  setCount = () => {    this.setState({count: this.state.count + 1})  }  render() {    return (      <div>        <SiblingA          count={this.state.count}        />        <SiblingB          onClick={this.setCount}        />      </div>    );  }}

6、Context

通常一个前端应用会有一些"全局"性质的数据,比如当前登陆的用户信息、ui主题、用户选择的语言等等。

这些全局数据,很多组件可能都会用到,当组件层级很深时,用我们之前的方法,就得通过props一层一层传递下去,这显然太麻烦了,看下面的示例:​​​​​​​

class App extends React.Component {  render() {    return <Toolbar theme="dark" />;  }}function Toolbar(props) {  return (    <div>      <ThemedButton theme={props.theme} />    </div>  );}class ThemedButton extends React.Component {  render() {    return <Button theme={this.props.theme} />;  }}

上面的例子,为了让我们的Button元素拿到主题色,我们必须把theme作为props,从App传到Toolbar,再从Toolbar传到ThemedButton,最后Button从父组件ThemedButton的props里终于拿到了主题theme。

假如我们不同组件里都有用到Button,就得把theme向这个例子一样到处层层传递,麻烦至极。

因此react为我们提供了一个新api:Context,我们用Context改写下上例:​​​​​​​

const ThemeContext = React.createContext('light');class App extends React.Component {  render() {    return (      <ThemeContext.Provider value="dark">        <Toolbar />      </ThemeContext.Provider>    );  }}function Toolbar() {  return (    <div>      <ThemedButton />    </div>  );}class ThemedButton extends React.Component {  static contextType = ThemeContext;  render() {    return <Button theme={this.context} />;  }}

简单的解析一下:

1、React.createContext创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider组件中读取当前的context值

2、Context.Provider: 每一个Context对象都有一个Provider属性,这个属性是一个react组件。

在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value的prop传递给所有内部组件,每当value的值发生变化时,Provider内部的组件都会根据新value值重新渲染

3、那内部的组件该怎么使用这个context对象里的东西呢?
a、假如内部组件是用class声明的有状态组件:我们可以把Context对象赋值给这个类的属性contextType,如上面所示的ThemedButton组件​​​​​​​

    class ThemedButton extends React.Component {      static contextType = ThemeContext;      render() {        const value = this.context        return <Button theme={value} />;      }    }

b、假如内部组件是用function创建的无状态组件:我们可以使用Context.Consumer,这也是Context对象直接提供给我们的组件,这个组件接受一个函数作为自己的child,这个函数的入参就是context的value,并返回一个react组件。可以将上面的ThemedButton改写下:​​​​​​​

    function ThemedButton {
        return (
​​​​​​​            <ThemeContext.Consumer>
                {value => <Button theme={value} />}
​​​​​​​            </ThemeContext.Consumer>
        )    }

最后提一句,context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context。如果只是单纯为了解决层级很深的props传递,可以直接用component composition。

7、Portals

Portals也是react提供的新特性,虽然它并不是用来解决组件通信问题的,但因为它也涉及到了组件通信的问题,所以我也把它列在我们的十种方法里面。

Portals的主要应用场景是:当两个组件在react项目中是父子组件的关系,但在HTML DOM里并不想是父子元素的关系。

举个例子,有一个父组件Parent,它里面包含了一个子组件Tooltip,虽然在react层级上它们是父子关系,但我们希望子组件Tooltip渲染的元素在DOM中直接挂载在body节点里,而不是挂载在父组件的元素里。这样就可以避免父组件的一些样式(如overflow:hidden、z-index、position等)导致子组件无法渲染成我们想要的样式。

如下图所示,父组件是这个红色框的范围,并且设置了overflow:hidden,这时候我们的Tooltip元素超出了红色框的范围就被截断了。

怎么用portals解决呢?

首先,修改html文件,给portals增加一个节点。​​​​​​​

<html>    <body>        <div id="react-root"></div>        <div id="portal-root"></div>    </body></html>

然后我们创建一个可复用的portal容器,这里使用了react hooks的语法,看不懂的先过去看下我另外一篇讲解react hooks的文章:30分钟精通React今年最劲爆的新特性——React Hooks​​​​​​​

import { useEffect } from "react";import { createPortal } from "react-dom";const Portal = ({children}) => {  const mount = document.getElementById("portal-root");  const el = document.createElement("div");  useEffect(() => {    mount.appendChild(el);    return () => mount.removeChild(el);  }, [el, mount]);  return createPortal(children, el)};export default Portal;

最后在父组件中使用我们的portal容器组件,并将Tooltip作为children传给portal容器组件。​​​​​​​

const Parent = () => {  const [coords, setCoords] = useState({});  return <div style={{overflow: "hidden"}}>      <Button>        Hover me      </Button>      <Portal>        <Tooltip coords={coords}>          Awesome content that is never cut off by its parent container!         </Tooltip>      </Portal>  </div>}

这样就ok啦,虽然父组件仍然是overflow: hidden,但我们的Tooltip再也不会被截断了,因为它直接超脱了,它渲染到body节点下的<div id="portal-root"></div>里去了。

总结下适用的场景: Tooltip、Modal、Popup、Dropdown等等

8、Global Variables - 全局变量

哈哈,这也不失为一个可行的办法啊。当然你最好别用这种方法。

class ComponentA extends React.Component {    handleClick = () => window.a = 'test'    ...}class ComponentB extends React.Component {    render() {        return <div>{window.a}</div>    }}

9、Observer Pattern - 观察者模式

观察者模式是软件设计模式里很常见的一种,它提供了一个订阅模型,假如一个对象订阅了某个事件,当那个事件发生的时候,这个对象将收到通知。

这种模式对于我们前端开发者来说是最不陌生的了,因为我们经常会给某些元素添加绑定事件,会写很多的event handlers,比如给某个元素添加一个点击的响应事件elm.addEventListener('click', handleClickEvent),每当elm元素被点击时,这个点击事件会通知elm元素,然后我们的回调函数handleClickEvent会被执行。

这个过程其实就是一个观察者模式的实现过程。

那这种模式跟我们讨论的react组件通信有什么关系呢?当我们有两个完全不相关的组件想要通信时,就可以利用这种模式,其中一个组件负责订阅某个消息,而另一个元素则负责发送这个消息。

javascript提供了现成的api来发送自定义事件: CustomEvent,我们可以直接利用起来。

首先,在ComponentA中,我们负责接受这个自定义事件:

class ComponentA extends React.Component {    componentDidMount() {        document.addEventListener('myEvent', this.handleEvent)    }    componentWillUnmount() {        document.removeEventListener('myEvent', this.handleEvent)    }        handleEvent = (e) => {        console.log(e.detail.log)  //i'm zach    }}

然后,ComponentB中,负责在合适的时候发送该自定义事件:

class ComponentB extends React.Component {    sendEvent = () => {        document.dispatchEvent(new CustomEvent('myEvent', {          detail: {             log: "i'm zach"          }        }))    }        render() {        return <button onClick={this.sendEvent}>Send</button>    }}

这样我们就用观察者模式实现了两个不相关组件之间的通信。当然现在的实现有个小问题,我们的事件都绑定在了document上,这样实现起来方便,但很容易导致一些冲突的出现,所以我们可以小小的改良下,独立一个小模块EventBus专门这件事:

class EventBus {    constructor() {        this.bus = document.createElement('fakeelement');    }    addEventListener(event, callback) {        this.bus.addEventListener(event, callback);    }    removeEventListener(event, callback) {        this.bus.removeEventListener(event, callback);    }    dispatchEvent(event, detail = {}){        this.bus.dispatchEvent(new CustomEvent(event, { detail }));    }}export default new EventBus

然后我们就可以愉快的使用它了,这样就避免了把所有事件都绑定在document上的问题:

import EventBus from './EventBus'class ComponentA extends React.Component {    componentDidMount() {        EventBus.addEventListener('myEvent', this.handleEvent)    }    componentWillUnmount() {        EventBus.removeEventListener('myEvent', this.handleEvent)    }        handleEvent = (e) => {        console.log(e.detail.log)  //i'm zach    }}class ComponentB extends React.Component {    sendEvent = () => {        EventBus.dispatchEvent('myEvent', {log: "i'm zach"}))    }        render() {        return <button onClick={this.sendEvent}>Send</button>    }}

最后我们也可以不依赖浏览器提供的api,手动实现一个观察者模式,或者叫pub/sub,或者就叫EventBus。

function EventBus() {  const subscriptions = {};  this.subscribe = (eventType, callback) => {    const id = Symbol('id');    if (!subscriptions[eventType]) subscriptions[eventType] = {};    subscriptions[eventType][id] = callback;    return {      unsubscribe: function unsubscribe() {        delete subscriptions[eventType][id];        if (Object.getOwnPropertySymbols(subscriptions[eventType]).length === 0) {          delete subscriptions[eventType];        }      },    };  };  this.publish = (eventType, arg) => {    if (!subscriptions[eventType]) return;    Object.getOwnPropertySymbols(subscriptions[eventType])      .forEach(key => subscriptions[eventType][key](arg));  };}export default EventBus;

10、Reduxmobx/zustand/jotai/recoil/valtio

当项目比较大,前面讲的9种方法已经不能很好满足项目需求时,才会使用redux等状态管理库。

【前端状态管理】React 状态管理工具如何选 context/redux/mobx/zustand/jotai/recoil/valtio_AwesomeDevin的博客-CSDN博客

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

10种React组件之间通信的方法 的相关文章

随机推荐

  • git忽略指定文件夹

    git忽略指定文件夹 如下结构 总共有三个文件夹 假设要忽略第一层的B文件夹 在目录下新建一个 gitignore文件 并填写下面内容 B 假设要忽略第一层的A文件夹 在文件中填写A 的话 会把B文件夹下的A文件夹也忽略了 这个时候可以加上
  • 要称王,先做行业破坏者

    author skate time 2010 06 18 高端阅读78期 原标题为 世界 油王 的职场启示 我的人生 狠 字当头 有极强的故事性 白手起家 狂赚几亿美元 后遭朋友暗算 被踢出一手创办并成功发展40年的公司 同期不得不应对麻烦
  • Linux nrm 运行失败,解决:npm中 下载速度慢 和(无法将“nrm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确, 然后再试一次)...

    1 解决下载速度 因为我们npm下载默认是 连接国外的服务器 所以网速不是特别好的时候 可能下不了包 安装nrm 使用 npm i nrm g 我们的一般工具包都是下载到全局 安装完毕之后 可以运行 命令 nrm ls ls 表示 list
  • Django-Model层ORM之查询操作(六)

    目录 一 Django查询相关API all 查询所有记录 返回一个集合对象 filter 属性 根据条件查询 返回一个集合对象 first 和 last 查询第一个和最后一个记录 返回单个对象 get id 2 根据id查询 返回一个对象
  • Ubuntu 15.04 下编译Caffe2

    深度学习大神贾扬清在四月底发布了最新框架Caffe2 最近在Ubuntu15 04下编译了它的源代码 遇到一些坑 记录下来以供参考 基本安装次序如官网所述 https caffe2 ai docs getting started html
  • k8s Trouble Shooting 故障排除

    本文要讲的是k8s的故障排除 比较浅 最近刚入门 主要涵盖的内容是查看k8s对象的当前运行时信息 对于服务 容器的问题是如何诊断的 对于某些复杂的问题例如pod调度问题是如何排查的 1 查看系统的Event事件 在对象资源 pod serv
  • 一起写一个 Web 服务器

    http my oschina net leejun2005 blog 486771 一起写一个 Web 服务器 2 2015 06 06 实践项目 9 评论 Web服务器 分享到 8 本文由 伯乐在线 高世界 翻译 艾凌风 校稿 未经许可
  • java实现评论功能_Java实现评论回复功能的完整步骤

    前言 使用递归循环开发评论回复功能 适用于大部分的简单单体应用 评论功能或许是大多数的单体应用之中会用到的功能 我们会在自己所开发的项目之中进行集成该功能 大多数时候我们会将评论功能划分成以下几种 单一型 嵌套型 两层型 一 分类方式 1
  • SAP B/P 初步研究(二)

    从开发人员角度来看 B P客户创建可以试用两种方法 第一种是使用BAPI FUNCTION 第二种是使用BAPI CALL METHOD 个人更倾向于使用METHOD 因为METHOD方法只需要填充一个嵌套结构就可以实现B P所有业务视图的
  • 【STM32】制作一个bootloader

    工作环境 STM32CubeMX Keil 相关环境准备这里就不介绍了 bootloader是什么 bootloader就是单片机启动时候运行的一段小程序 这段程序负责单片机固件的更新 也就是单片机选择性的自己给自己下载程序 可以更新 可以
  • Linux C 系统编程 2-1 进程管理 进程环境

    该系列文章总纲链接 专题分纲目录 LinuxC 系统编程 本章节思维导图如下所示 思维导图会持续迭代 第一层 第二层 1 进程的启动和退出 1 1 流程 程序启动 gt 程序加载 地址分配 gt 程序退出 1 程序启动 对于二进制文件 如果
  • 浅谈State状态模式

    一 前言 状态模式在某些场合中使用是非常方便的 什么叫做状态 如果大家学过 编译原理 就会明白DFA M和NFA M 在确定有限状态机和非确定有限状态机中 状态就是最小的单元 当满足某种条件的时候 状态就会发生改变 我们可以把时间中的一个时
  • OSPF的路由器角色

    IR internal router 区域内路由器 普通区域 BR backbone router 骨干区域路由器 位于骨干区域 至少一个接口在骨干区域 ABR area border rouder 区域边界路由器 作用是连接骨干区域和普通
  • Aixcode代码自动补全插件的安装和使用

    最近在技术公众号上看到大佬们说到一款代码自动补全的智能插件aixcode 官方是这样宣传的 智能代码提示 她用强大的深度学习引擎 能给出更加精确的代码提示 代码风格检查 她有代码风格智能检查能力 帮助开发者改善代码质量 编程模式学习 她能自
  • Verilog数据类型

    作者 anekin 原作网址 http blog sina com cn s blog 615047920100ih0k html Verilog HDL有下列四种基本的值 1 0 逻辑0或 假 状态 2 1 逻辑1或 真 状态 3 x X
  • 因果推断 - 反事实

    目录 基础知识 案例实战 版权 转载前请联系作者获得授权 声明 部分内容出自因果关系之梯 已获得原作者授权 参考书籍 The Book of Why Judea Pearl 基础知识 定义 对于包含外生变量 U U U和内生变量 X X
  • mysql 显示用户_在Mysql中如何显示所有用户?

    这是一个mysql初学者经常问到的一个问题 今天我们就带大家看看是如何在Mysql中显示所有用户的 通常我们在mysql中使用SHOW DATABASES可以显示所有的数据库 SHOW TABLES将会显示所有的数据表 那么你是不是会猜测显
  • 软件版本号讲解:什么是Alpha, Beta, RC,Release

    1 软件版本阶段说明 Alpha版 此版本表示该软件在此阶段主要是以实现软件功能为主 通常只在软件开发者内部交流 一般而言 该版本软件的Bug较多 需要继续修改 Beta版 该版本相对于 版已有了很大的改进 消除了严重的错误 但还是存在着一
  • LayUI图片上传接口

    前端样式 div class layui upload drag i class layui icon xe67c i p 点击上传 或将文件拖拽到此处 p div js var uploadInst upload render elem
  • 10种React组件之间通信的方法

    组件间通信方式总结 父组件 gt 子组件 1 Props 2 Instance Methods refs 子组件 gt 父组件 3 Callback Functions 回调函数 4 Event Bubbling 事件冒泡机制 兄弟组件之间