资讯专栏INFORMATION COLUMN

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

zhongmeizhi / 2818人阅读

摘要:如果某个组件订阅该事件太晚,那发布者之前所发布的该类事件,它都接收不到,而方案一和二的优点则在于,无论如何,组件都能拿到该的最终状态值有存在内存泄漏的风险。

原文地址 - 欢迎关注我的博客

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

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

实现一个视频播放器

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

视频窗口组件Screen

底部播放控件BottomCtrl

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

播放/暂停按钮BottomPlayBtn

进度控制条ProgressCtrl

音量按钮Volume

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

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

class MyVideo {
  render() {
    return (
      
) } } // 底部视频控件 class BottomCtrl { render() { return (
) } } // 视频窗口组件 class Screen { render() { return (
) } }

对于视频播放器而言,有一个很常见的交互,即当我们点击屏幕中心的播放按钮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 (
            
) } }

我们通过在祖先组件的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 (
            
                
                
            
        )
    }
}

接着在需要消费数据的地方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 => (
    {
        ({isPlay, updatePlayStatus}) => 
    } 
)

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

首先它像方案一一样,对数据作了集中控制管理,即把提供数据内容和修改数据的能力集中到了上层组件身上,使得上层组件成为唯一的Provider,供下层各处的消费者Consumer使用;

其次它无须像方案一一样繁琐地将props手动向下传递;

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

总结

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

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/53903.html

相关文章

  • 一个放器引发思考——谈谈React组件通信

    摘要:如果某个组件订阅该事件太晚,那发布者之前所发布的该类事件,它都接收不到,而方案一和二的优点则在于,无论如何,组件都能拿到该的最终状态值有存在内存泄漏的风险。 原文地址 - 欢迎关注我的博客 在我们react项目日常开发中,往往会遇到这样一个问题:如何去实现跨组件通信? 为了更好的理解此问题,接下来我们通过一个简单的栗子说明。 实现一个视频播放器 假设有一个这样的需求,需要我们去实现一个...

    Dean 评论0 收藏0
  • 关于Vue2一些值得推荐文章 -- 五、六月份

    摘要:五六月份推荐集合查看最新的请点击集前端最近很火的框架资源定时更新,欢迎一下。苏幕遮燎沈香宋周邦彦燎沈香,消溽暑。鸟雀呼晴,侵晓窥檐语。叶上初阳乾宿雨,水面清圆,一一风荷举。家住吴门,久作长安旅。五月渔郎相忆否。小楫轻舟,梦入芙蓉浦。 五、六月份推荐集合 查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。 苏...

    sutaking 评论0 收藏0
  • 关于Vue2一些值得推荐文章 -- 五、六月份

    摘要:五六月份推荐集合查看最新的请点击集前端最近很火的框架资源定时更新,欢迎一下。苏幕遮燎沈香宋周邦彦燎沈香,消溽暑。鸟雀呼晴,侵晓窥檐语。叶上初阳乾宿雨,水面清圆,一一风荷举。家住吴门,久作长安旅。五月渔郎相忆否。小楫轻舟,梦入芙蓉浦。 五、六月份推荐集合 查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。 苏...

    khs1994 评论0 收藏0
  • 前端面试题总结(js、html、小程序、React、ES6、Vue、算法、全栈热门视频资源)

    摘要:并总结经典面试题集各种算法和插件前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快速搭建项目。 本文是关注微信小程序的开发和面试问题,由基础到困难循序渐进,适合面试和开发小程序。并总结vue React html css js 经典面试题 集各种算法和插件、前端视频源码资源于一身的文档,优化项目,在浏览器端的层面上提升速度,帮助初中级前端工程师快...

    pumpkin9 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<