在我们react项目日常开发中,往往会遇到这样一个问题:如何去实现跨组件通信?

为了更好的理解此问题,接下来我们通过一个简单的栗子说明。

实现一个视频播放器

假设有一个这样的需求,需要我们去实现一个简易的视频播放器,基于对播放器的理解,我们可以把这个视频播放器大致分为如下几个部分:

  • 视频窗口组件Screen
  • 底部播放控件BottomCtrl

对于视频窗口组件,它包含一个播放/暂停按钮CenterPlayBtn;而底部播放控件又是由以下几种组件组合而成:

  • 播放/暂停按钮BottomPlayBtn
  • 进度控制条ProgressCtrl
  • 音量按钮Volume

于是乎它的构成应该如下图所示:

同样的,我们的组件组织方式应该也长这样:(这里简化了代码实现)

class MyVideo {render() {return (<div><Screen /><BottomCtrl /></div>)}
}// 底部视频控件
class BottomCtrl {render() {return (<div><BottomPlayBtn /><ProgressCtrl /><Volume /></div>)}
}// 视频窗口组件
class Screen {render() {return (<div><video /><ScreenPlayBtn /></div>)}
}
复制代码

对于视频播放器而言,有一个很常见的交互,即当我们点击屏幕中心的播放按钮CenterPlayBtn时,不仅需要改变自身的状态(隐藏起来),而且还要更新底部播放按钮BottomPlayBtn的样式

由于中心播放按钮与底部控件按钮分别属于ScreenBottomCtrl组件的部分,因此这就是一个很常见的跨组件通信问题:如何将CenterPlayBtn的状态同步到BottomPlayBtn?

方案一:祖先组件的状态管理

一个非常常用的方式,就是让祖先组件通过状态管理的方式把信息同步到其他子组件中:

class MyVideo {constructor(props) {super(props);this.state = {isPlay: false,}}updatePlayState = isPlay => {this.setState({ isPlay });}render() {const { isPlay } = this.state;return (<div><Screen updatePlayState={this.updatePlayState} isPlay={isPlay} /><BottomCtrl updatePlayState={this.updatePlayState} isPlay={isPlay} /></div>)}
}
复制代码

我们通过在祖先组件的state定义相应的状态,并把修改state的方法传递给了子组件,那么当一个子组件通过调用updatePlayState后,它所设置的新状态亦可通过react本身的state更新机制传递给其他的子组件,实现跨组件通信。

这种方案虽然简单,但在一些复杂的场景下却显得不够友好:

  1. 状态和方法需要通过层层props传递到相应的子组件,一旦组件嵌套过深,不好编写与维护,且对于中间传递的组件而言,增加了不必要的逻辑;
  2. 管理状态的祖先组件将变得更加臃肿。试想一下,假设我们为了实现两个嵌套很深的子组件的通信,却需要在祖先组件上去额外添加状态和方法,这增加了祖先组件的维护成本。

方案二:redux提供的跨组件通信能力

熟悉redux的童鞋都知道,redux提供的订阅发布机制,可以让我们实现任何两个组件的通信:首先我们需要在state上去添加一个key,在两个需要通信的组件上通过connect的封装,即可订阅key值的改变。

// CenterPlayBtn
class CenterPlayBtn {play() {this.props.updatePlayStatus();}
}const mapDispatchToProps = dispatch => {return {updatePlayStatus: isPlay => {dispatch(updatePlayStatus(isPlay))}}
}export default connect(null, mapDispatchToProps)(BottomPlayBtn)复制代码
class BottomPlayBtn {componentWillReceiveProps(nextProps) {if (this.props.isPlay !== nextProps.isPlay) {// do something}}
}const mapStateToProps = state => ({isPlay: state.isPlay
})export default connect(mapStateToProps, null)(BottomPlayBtn)
复制代码

使用redux的方式去实现跨组件通信是一种很常见的方式,在项目开发中也经常用到。那问题又来了,由于使用这种方案的前提是必须得在项目中加入redux,如果我的项目本来就比较简单,不需要使用到redux,难道为了实现两个组件简单的通信而要去做一系列redux的配置工作吗?这显然把简单的问题又复杂化了。

方案三:EventEmitter

EventEmitter也可以实现跨组件通信,当然这种基于事件订阅的设计模式本身也与react关系不大,但我们的项目很小的时候,使用EventEmitter也不失为一种简单且高效的方式:

class CenterPlayBtn {constructor(props) {super(props);event.on('pause', () => {// do something})}play() {event.emit('play');}
}class BottomPlayBtn {constructor(props) {super(props);event.on('play', () => {// do something})}pause() {event.emit('pause');}
}
复制代码

当然这种方案也是有缺陷的:

  • 组织方式过于离散。发送者emit与接收者on分散在各个组件里,如果不细看每个组件的代码,我们难以从整体去观察、跟踪、管理这些事件;
  • 有可能出现错过某个事件的情况。如果某个组件订阅该事件太晚,那发布者之前所发布的该类事件,它都接收不到,而方案一和二的优点则在于,无论如何,组件都能拿到该key的最终状态值;
  • 有存在内存泄漏的风险。如果组件销毁了而不及时取消订阅,那就有内存泄漏的风险;

方案四:利用react原生的context实现跨组件通信

原生react提供了context,它的原文描述是这样的:

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

简单来说就是react提供了一种方式,让你可以跨多层嵌套组件去访问数据,而不需要手动的将props一个一个地传递下去。通过这种方式我们也可以实现跨组件通信方式,这个方案和方案一很相似,但区别在于我们无需手动将props传递给经历的每一个中间层组件。更为具体的用法可以直接参考官网示例,下面只是抛砖引玉,给出个简单示例:

首先我们定义一个player-context.js文件

import { createContext } from 'react';
const PlayerContext = createContext();
export default PlayerContext;
复制代码

然后在MyVideo组件中使用PlayerContext.Provider:

import PlayerContext from './player-context';class MyVideo {constructor(props) {super(props);this.state = {isPlay: false,updatePlayState: this.updatePlayState,}}updatePlayState = isPlay => {this.setState({ isPlay });}render() {return (<PlayerContext.Provider value={this.state}><Screen /><BottomCtrl /></PlayerContext.Provider>)}
}
复制代码

接着在需要消费数据的地方CenterPlayBtnBottomPlayBtn中使用到它,这里只给出CenterPlayBtn的示例:

import PlayerContext from './player-context';class CenterPlayBtn {constructor(props) {super(props);}play() {this.props.updatePlayStatus(!this.props.isPlay);}componentWillReceiveProps(nextProps) {if (this.props.isPlay !== nextProps.isPlay) {// do something...}}
}export default props => (<PlayerContext.Consumer>{({isPlay, updatePlayStatus}) => <CenterPlayBtn {...props} isPlay={isPlay} updatePlayStatus={updatePlayStatus} />}
</PlayerContext.Consumer>)复制代码

其实个人认为这种方案是方案一的“增强版”:

  • 首先它像方案一一样,对数据作了集中控制管理,即把提供数据内容和修改数据的能力集中到了上层组件身上,使得上层组件成为唯一的Provider,供下层各处的消费者Consumer使用;
  • 其次它无须像方案一一样繁琐地将props手动向下传递;

总得来说,如果你的项目没有使用到redux的话,使用context是个不错的选择。

总结

上面列举的方案各有优劣,我们很难去判定哪种方案是最好的,而真正重要的,是要学会分析哪个场景下使用哪种方案更佳。

btw,其实跨组件通信的方式多种多样,远不止这些,本人才疏学浅,这里只能列举出一些自己常用的解决方案,希望此文能抛砖引玉,引出更棒的方案和见解:)

一个播放器引发的思考——谈谈React跨组件通信相关推荐

  1. 从0开始写一个播放器系列-开篇

    从0开始写一个播放器系列-开篇 阅读本系列博客所需要具备的知识: js, ts , canvas Api , DOM, webpack, 不懂也没有太大的关系, 我会捎带着写出来, 剩下的自行百度 第 ...

  2. 一个小程序引发的思考

    既然是一个小程序引发的思考,那么我们就先看看这个小程序,看看他有何神奇之处: namespace ConsoleApplication1 {class Program{static void Main ...

  3. 使用AVPlayer制作一个播放器

    代码地址如下: http://www.demodashi.com/demo/11685.html AVPlayer 是一个强大的视频播放器,可以播放多种格式的视频,缺点是没有控制界面,需要自己去实现. ...

  4. 制作一个播放器(二)

    制作一个播放器(一) 制作一个播放器(二) 接着上一章的热身,咱们继续写播放器.上一篇中咱们用的是句柄的方式来播放视频.实际开发中,我们更多的是把数据给回调出来,这样更好的去显示视频,处理视频.这期, ...

  5. 使用纯css做一个播放器

    首先,贴出成品图,如下: 可以发现播放器的基本形状有了,但是需要精确到每一个方向,不能溢出,就得以如下的方式写,贴出静态代码: html如下: <!DOCTYPE html> <ht ...

  6. 一个分组查询引发的思考

    一个分组查询引发的思考 我们在看项目代码或者SQL语句时, 往往会看到很多非常复杂的业务或者SQL 那么问题来了. 复杂SQL是如何写成的? 下面通过一个数据展示的需求来体会到复杂的SQL是如何书写的 ...

  7. 使用go语言编写一个播放器

    使用go语言编写一个播放器,直接看效果 整体思路是使用ffmpeg解码,然后使用xui进行渲染,音频播放暂时还没调通,以下是源码 package mainimport ("github.co ...

  8. 制作一个播放器(一)

    制作一个播放器(一) 制作一个播放器(二) 开发前准备 开发平台:windows 开发语言:C++(Qt) 开发工具:Visual Studio 2019 cmake  Qt5.15.2 做个小广告 ...

  9. C#——窗体程序声明一个播放器接口IPlayer,包含5个接口方法:播放、停止、暂停、上一首和下一首。

    声明一个播放器接口IPlayer,包含5个接口方法:播放.停止.暂停.上一首和下一首.设计一个窗体程序,在该程序中定义一个MP3播放器类和一个AVI播放器类,以实现该接口,最后创建相应类的实例测试程序 ...

最新文章

  1. 梅花桩上练真功,腾讯公布机器人移动技术探索新突破
  2. 2008中国国际计算机信息及网络安全展览会
  3. Android View 测量流程(Measure)完全解析
  4. JavaScript 开发10个实用技巧
  5. 伪类如何动态在html设置样式,用js实现before和after伪类的样式修改的示例代码
  6. linux查看帮助信息,命令帮助信息的获取
  7. 单片机成长之路(51基础篇) - 013 MCS-51单片机控制详解–T2MOD
  8. C#调用windows API实现 smallpdf客户端程序进行批量压缩
  9. matlab iri模型,IRI-2016 Matlab 使用教程
  10. 快速选择(QuickSelect)的平均时间复杂度分析
  11. Vue2基础-el与data的两种写法(HTML版)
  12. P2P: Tuning Pre-trained Image Models for Point Cloud Analysis with Point-to-Pixel Prompting
  13. 品质精选丨一名合格运营必备的8款工具,你都会用了吗?
  14. 首次公开,300多页阿里百亿级系统架构设计实录
  15. Mat 读取数据 及效率对比
  16. linux内核版本和发行版本有何区别
  17. eWebSoft在线 HTML 编辑器的使用说明
  18. RHEL8 新特性和安装方法
  19. Machine Learning课堂笔记之Computing Parameters Analytically
  20. Carbide 项目blink问题

热门文章

  1. 浅析Kubernetes资源管理
  2. hibernate中antlr对于hql的词法分析源码解析
  3. 7 学大厂,拓展基础组件封装思路 BAT?TMD
  4. 工程师软技能3:如何学习
  5. 图片绑定file上传并获取图片的base64
  6. vue中标签自定义属性的使用
  7. golang 代理地址
  8. PHP中对数组进行分页处理的原理及分页实例
  9. CSS分别设置Input样式(按input类型
  10. 高效实用Kafka-Kafka集群维护(分区平衡机制、kafka分区日志迁移)