一个播放器引发的思考——谈谈React跨组件通信
在我们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
的样式
由于中心播放按钮与底部控件按钮分别属于Screen
、BottomCtrl
组件的部分,因此这就是一个很常见的跨组件通信问题:如何将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更新机制传递给其他的子组件,实现跨组件通信。
这种方案虽然简单,但在一些复杂的场景下却显得不够友好:
- 状态和方法需要通过层层props传递到相应的子组件,一旦组件嵌套过深,不好编写与维护,且对于中间传递的组件而言,增加了不必要的逻辑;
- 管理状态的祖先组件将变得更加臃肿。试想一下,假设我们为了实现两个嵌套很深的子组件的通信,却需要在祖先组件上去额外添加状态和方法,这增加了祖先组件的维护成本。
方案二: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>)}
}
复制代码
接着在需要消费数据的地方CenterPlayBtn
和BottomPlayBtn
中使用到它,这里只给出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跨组件通信相关推荐
- 从0开始写一个播放器系列-开篇
从0开始写一个播放器系列-开篇 阅读本系列博客所需要具备的知识: js, ts , canvas Api , DOM, webpack, 不懂也没有太大的关系, 我会捎带着写出来, 剩下的自行百度 第 ...
- 一个小程序引发的思考
既然是一个小程序引发的思考,那么我们就先看看这个小程序,看看他有何神奇之处: namespace ConsoleApplication1 {class Program{static void Main ...
- 使用AVPlayer制作一个播放器
代码地址如下: http://www.demodashi.com/demo/11685.html AVPlayer 是一个强大的视频播放器,可以播放多种格式的视频,缺点是没有控制界面,需要自己去实现. ...
- 制作一个播放器(二)
制作一个播放器(一) 制作一个播放器(二) 接着上一章的热身,咱们继续写播放器.上一篇中咱们用的是句柄的方式来播放视频.实际开发中,我们更多的是把数据给回调出来,这样更好的去显示视频,处理视频.这期, ...
- 使用纯css做一个播放器
首先,贴出成品图,如下: 可以发现播放器的基本形状有了,但是需要精确到每一个方向,不能溢出,就得以如下的方式写,贴出静态代码: html如下: <!DOCTYPE html> <ht ...
- 一个分组查询引发的思考
一个分组查询引发的思考 我们在看项目代码或者SQL语句时, 往往会看到很多非常复杂的业务或者SQL 那么问题来了. 复杂SQL是如何写成的? 下面通过一个数据展示的需求来体会到复杂的SQL是如何书写的 ...
- 使用go语言编写一个播放器
使用go语言编写一个播放器,直接看效果 整体思路是使用ffmpeg解码,然后使用xui进行渲染,音频播放暂时还没调通,以下是源码 package mainimport ("github.co ...
- 制作一个播放器(一)
制作一个播放器(一) 制作一个播放器(二) 开发前准备 开发平台:windows 开发语言:C++(Qt) 开发工具:Visual Studio 2019 cmake Qt5.15.2 做个小广告 ...
- C#——窗体程序声明一个播放器接口IPlayer,包含5个接口方法:播放、停止、暂停、上一首和下一首。
声明一个播放器接口IPlayer,包含5个接口方法:播放.停止.暂停.上一首和下一首.设计一个窗体程序,在该程序中定义一个MP3播放器类和一个AVI播放器类,以实现该接口,最后创建相应类的实例测试程序 ...
最新文章
- 梅花桩上练真功,腾讯公布机器人移动技术探索新突破
- 2008中国国际计算机信息及网络安全展览会
- Android View 测量流程(Measure)完全解析
- JavaScript 开发10个实用技巧
- 伪类如何动态在html设置样式,用js实现before和after伪类的样式修改的示例代码
- linux查看帮助信息,命令帮助信息的获取
- 单片机成长之路(51基础篇) - 013 MCS-51单片机控制详解–T2MOD
- C#调用windows API实现 smallpdf客户端程序进行批量压缩
- matlab iri模型,IRI-2016 Matlab 使用教程
- 快速选择(QuickSelect)的平均时间复杂度分析
- Vue2基础-el与data的两种写法(HTML版)
- P2P: Tuning Pre-trained Image Models for Point Cloud Analysis with Point-to-Pixel Prompting
- 品质精选丨一名合格运营必备的8款工具,你都会用了吗?
- 首次公开,300多页阿里百亿级系统架构设计实录
- Mat 读取数据 及效率对比
- linux内核版本和发行版本有何区别
- eWebSoft在线 HTML 编辑器的使用说明
- RHEL8 新特性和安装方法
- Machine Learning课堂笔记之Computing Parameters Analytically
- Carbide 项目blink问题