一個播放器引起的思考——談談React跨組件通訊

原文地址 - 歡迎關注個人博客javascript

在咱們react項目平常開發中,每每會遇到這樣一個問題:如何去實現跨組件通訊?html

爲了更好的理解此問題,接下來咱們經過一個簡單的栗子說明。java

實現一個視頻播放器

假設有一個這樣的需求,須要咱們去實現一個簡易的視頻播放器,基於對播放器的理解,咱們能夠把這個視頻播放器大體分爲以下幾個部分:react

  • 視頻窗口組件Screen
  • 底部播放控件BottomCtrl

對於視頻窗口組件,它包含一個播放/暫停按鈕CenterPlayBtn;而底部播放控件又是由如下幾種組件組合而成:git

  • 播放/暫停按鈕BottomPlayBtn
  • 進度控制條ProgressCtrl
  • 音量按鈕Volume

因而乎它的構成應該以下圖所示:
github

一樣的,咱們的組件組織方式應該也長這樣:(這裏簡化了代碼實現)redux

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?ide

方案一:祖先組件的狀態管理

一個很是經常使用的方式,就是讓祖先組件經過狀態管理的方式把信息同步到其餘子組件中:this

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,其實跨組件通訊的方式多種多樣,遠不止這些,本人才疏學淺,這裏只能列舉出一些本身經常使用的解決方案,但願此文能拋磚引玉,引出更棒的方案和看法:)

相關文章
相關標籤/搜索