react組件之間通信

很感謝https://segmentfault.com/u/cwl提供的答案
React 組件間通訊

說(shuō) React 組件間通訊之前,我們先來(lái)討論一下 React 組件究竟有多少種層級(jí)間的關(guān)系。假設(shè)我們開(kāi)發(fā)的項(xiàng)目是一個(gè)純 React 的項(xiàng)目,那我們項(xiàng)目應(yīng)該有如下類(lèi)似的關(guān)系:

父子:Parent 與 Child_1、Child_2、Child_1_1、Child_1_2、Child_2_1

兄弟:Child_1 與 Child_2、Child_1_1 與 Child_2、etc.

針對(duì)這些關(guān)系,我們將來(lái)好好討論一下這些關(guān)系間的通訊方式。

(在 React 中,React 組件之間的關(guān)系為從屬關(guān)系,與 DOM 元素之間的父子關(guān)系有所不同,下面只是為了說(shuō)明方便,將 React 組件的關(guān)系類(lèi)比成父子關(guān)系進(jìn)行闡述)

父組件向子組件通訊

通訊是單向的,數(shù)據(jù)必須是由一方傳到另一方。在 React 中,父組件可以向子組件通過(guò)傳 props 的方式,向子組件進(jìn)行通訊。

class Parent extends Component{
  state = {
    msg: 'start'
  };

  componentDidMount() {
    setTimeout(() => {
      this.setState({
        msg: 'end'
      });
    }, 1000);
  }

  render() {
    return <Child_1 msg={this.state.msg} />;
  }
}

class Child_1 extends Component{
  render() {
    return <p>{this.props.msg}</p>
  }
}

如果父組件與子組件之間不止一個(gè)層級(jí),如 Parent 與 Child_1_1 這樣的關(guān)系,可通過(guò) ... 運(yùn)算符(Object 剩余和展開(kāi)屬性),將父組件的信息,以更簡(jiǎn)潔的方式傳遞給更深層級(jí)的子組件。通過(guò)這種方式,不用考慮性能的問(wèn)題,通過(guò) babel 轉(zhuǎn)義后的 ... 運(yùn)算符 性能和原生的一致,且上級(jí)組件 props 與 state 的改變,會(huì)導(dǎo)致組件本身及其子組件的生命周期改變,

// 通過(guò) ... 運(yùn)算符 向 Child_1_1 傳遞 Parent 組件的信息

class Child_1 extends Component{
  render() {
    return <div>
      <p>{this.props.msg}</p>
      <Child_1_1 {...this.props}/>
    </div>
  }
}

class Child_1_1 extends Component{
  render() {
    return <p>{this.props.msg}</p>
  }
}

子組件向父組件通訊

在上一個(gè)例子中,父組件可以通過(guò)傳遞 props 的方式,自頂而下向子組件進(jìn)行通訊。而子組件向父組件通訊,同樣也需要父組件向子組件傳遞 props 進(jìn)行通訊,只是父組件傳遞的,是作用域?yàn)楦附M件自身的函數(shù),子組件調(diào)用該函數(shù),將子組件想要傳遞的信息,作為參數(shù),傳遞到父組件的作用域中。

class Parent extends Component{
  state = {
    msg: 'start'
  };
  
  transferMsg(msg) {
    this.setState({
      msg
    });
  }

  render() {
    return <div>
        <p>child msg: {this.state.msg}</p>
        <Child_1 transferMsg = {msg => this.transferMsg(msg)} />
      </div>;
  }
}

class Child_1 extends Component{
  componentDidMount() {
    setTimeout(() => {
      this.props.transferMsg('end')
    }, 1000);
  }

  render() {
    return <div>
      <p>child_1 component</p>
    </div>
  }
}

在上面的例子中,我們使用了 箭頭函數(shù),將父組件的 transferMsg 函數(shù)通過(guò) props 傳遞給子組件,得益于箭頭函數(shù),保證子組件在調(diào)用 transferMsg 函數(shù)時(shí),其內(nèi)部 this 仍指向父組件。

當(dāng)然,對(duì)于層級(jí)比較深的子組件與父組件之間的通訊,仍可使用 ... 運(yùn)算符,將父組件的調(diào)用函數(shù)傳遞給子組件,具體方法和上面的例子類(lèi)似。

兄弟組件間通訊

對(duì)于沒(méi)有直接關(guān)聯(lián)關(guān)系的兩個(gè)節(jié)點(diǎn),就如 Child_1 與 Child_2 之間的關(guān)系,他們唯一的關(guān)聯(lián)點(diǎn),就是擁有相同的父組件。參考之前介紹的兩種關(guān)系的通訊方式,如果我們向由 Child_1 向 Child_2 進(jìn)行通訊,我們可以先通過(guò) Child_1 向 Parent 組件進(jìn)行通訊,再由 Parent 向 Child_2 組件進(jìn)行通訊,所以有以下代碼。

class Parent extends Component{
  state = {
    msg: 'start'
  };

  transferMsg(msg) {
    this.setState({
      msg
    });
  }

  componentDidUpdate() {
    console.log('Parent update');
  }

  render() {
    return (
      <div>
        <Child_1 transferMsg = {msg => this.transferMsg(msg)} />
        <Child_2 msg = {this.state.msg} />
      </div>
    );
  }
}

class Child_1 extends Component{
  componentDidMount() {
    setTimeout(() => {
      this.props.transferMsg('end')
    }, 1000);
  }

  componentDidUpdate() {
    console.log('Child_1 update');
  }

  render() {
    return <div>
      <p>child_1 component</p>
    </div>
  }
}

class Child_2 extends Component{
  componentDidUpdate() {
    console.log('Child_2 update');
  }

  render() {
    return <div>
      <p>child_2 component: {this.props.msg}</p>
      <Child_2_1 />
    </div>
  }
}

class Child_2_1 extends Component{
  componentDidUpdate() {
    console.log('Child_2_1 update');
  }

  render() {
    return <div>
      <p>child_2_1 component</p>
    </div>
  }
}

然而,這個(gè)方法有一個(gè)問(wèn)題,由于 Parent 的 state 發(fā)生變化,會(huì)觸發(fā) Parent 及從屬于 Parent 的子組件的生命周期,所以我們?cè)诳刂婆_(tái)中可以看到,在各個(gè)組件中的 componentDidUpdate 方法均被觸發(fā)。

有沒(méi)有更好的解決方式來(lái)進(jìn)行兄弟組件間的通訊,甚至是父子組件層級(jí)較深的通訊的呢?

觀察者模式

在傳統(tǒng)的前端解耦方面,觀察者模式作為比較常見(jiàn)一種設(shè)計(jì)模式,大量使用在各種框架類(lèi)庫(kù)的設(shè)計(jì)當(dāng)中。即使我們?cè)趯?xiě) React,在寫(xiě) JSX,我們核心的部分還是 JavaScript。

觀察者模式也叫 發(fā)布者-訂閱者模式,發(fā)布者發(fā)布事件,訂閱者監(jiān)聽(tīng)事件并做出反應(yīng),對(duì)于上面的代碼,我們引入一個(gè)小模塊,使用觀察者模式進(jìn)行改造。

import eventProxy from '../eventProxy'

class Parent extends Component{
  render() {
    return (
      <div>
        <Child_1/>
        <Child_2/>
      </div>
    );
  }
}
// componentDidUpdate 與 render 方法與上例一致
class Child_1 extends Component{
  componentDidMount() {
    setTimeout(() => {
      // 發(fā)布 msg 事件
      eventProxy.trigger('msg', 'end');
    }, 1000);
  }
}
// componentDidUpdate 方法與上例一致
class Child_2 extends Component{
  state = {
    msg: 'start'
  };

  componentDidMount() {
      // 監(jiān)聽(tīng) msg 事件
    eventProxy.on('msg', (msg) => {
      this.setState({
        msg
      });
    });
  }

  render() {
    return <div>
      <p>child_2 component: {this.state.msg}</p>
      <Child_2_1 />
    </div>
  }
}

我們?cè)?child_2 組件的 componentDidMount 中訂閱了 msg 事件,并在 child_1 componentDidMount 中,在 1s 后發(fā)布了 msg 事件,child_2 組件對(duì) msg 事件做出相應(yīng),更新了自身的 state,我們可以看到,由于在整個(gè)通訊過(guò)程中,只改變了 child_2 的 state,因而只有 child_2 和 child_2_1 出發(fā)了一次更新的生命周期。

而上面代碼中,神奇的 eventProxy.js 究竟是怎樣的一回事呢?

// eventProxy.js
'use strict';
const eventProxy = {
  onObj: {},
  oneObj: {},
  on: function(key, fn) {
    if(this.onObj[key] === undefined) {
      this.onObj[key] = [];
    }

    this.onObj[key].push(fn);
  },
  one: function(key, fn) {
    if(this.oneObj[key] === undefined) {
      this.oneObj[key] = [];
    }

    this.oneObj[key].push(fn);
  },
  off: function(key) {
    this.onObj[key] = [];
    this.oneObj[key] = [];
  },
  trigger: function() {
    let key, args;
    if(arguments.length == 0) {
      return false;
    }
    key = arguments[0];
    args = [].concat(Array.prototype.slice.call(arguments, 1));

    if(this.onObj[key] !== undefined
      && this.onObj[key].length > 0) {
      for(let i in this.onObj[key]) {
        this.onObj[key][i].apply(null, args);
      }
    }
    if(this.oneObj[key] !== undefined
      && this.oneObj[key].length > 0) {
      for(let i in this.oneObj[key]) {
        this.oneObj[key][i].apply(null, args);
        this.oneObj[key][i] = undefined;
      }
      this.oneObj[key] = [];
    }
  }
};
export default eventProxy;

eventProxy 中,總共有 on、one、off、trigger 這 4 個(gè)函數(shù):

on、one:on 與 one 函數(shù)用于訂閱者監(jiān)聽(tīng)相應(yīng)的事件,并將事件響應(yīng)時(shí)的函數(shù)作為參數(shù),on 與 one 的唯一區(qū)別就是,使用 one 進(jìn)行訂閱的函數(shù),只會(huì)觸發(fā)一次,而 使用 on 進(jìn)行訂閱的函數(shù),每次事件發(fā)生相應(yīng)時(shí)都會(huì)被觸發(fā)。
trigger:trigger 用于發(fā)布者發(fā)布事件,將除第一參數(shù)(事件名)的其他參數(shù),作為新的參數(shù),觸發(fā)使用 one 與 on 進(jìn)行訂閱的函數(shù)。
off:用于解除所有訂閱了某個(gè)事件的所有函數(shù)。
Flux 與 Redux

Flux 作為 Facebook 發(fā)布的一種應(yīng)用架構(gòu),他本身是一種模式,而不是一種框架,基于這個(gè)應(yīng)用架構(gòu)模式,在開(kāi)源社區(qū)上產(chǎn)生了眾多框架,其中最受歡迎的就是我們即將要說(shuō)的 Redux。更多關(guān)于 Flux 和 Redux 的介紹這里就不一一展開(kāi),有興趣的同學(xué)可以好好看看 Flux 官方介紹、Flux 架構(gòu)入門(mén)教程–阮一峰等相關(guān)資料。
下面將來(lái)好好聊聊 Redux 在組件間通訊的方式。

Flux 需要四大部分組成:Dispatcher、Stores、Views/Controller-Views、Actions,其中的 Views/Controller-Views 可以理解為我們上面所說(shuō)的 Parent 組件,其作用是從 state 當(dāng)中獲取到相應(yīng)的數(shù)據(jù),并將其傳遞給他的子組件(descendants)。而另外 3 個(gè)部分,則是由 Redux 來(lái)提供了。

// 該例子主要對(duì)各組件的 componentDidMount 進(jìn)行改造,其余部分一致

import {createStore} from 'redux'

function reducer(state = {}, action) {
  return action;
}

let store = createStore(reducer);

class Child_1 extends Component{
  componentDidMount() {
    setTimeout(() => {
      store.dispatch({
        type: 'child_2',
        data: 'hello'
      })
    }, 1000);

    setTimeout(() => {
      store.dispatch({
        type: 'child_2_1',
        data: 'bye'
      })
    }, 2000);
  }
}

class Child_2 extends Component{
  state = {
    msg: 'start'
  };
  
  componentDidUpdate() {
    console.log('Child_2 update', store.getState());
  }

  componentDidMount() {
    store.subscribe(() => {
      let state = store.getState();
      if (state.type === 'child_2') {
        this.setState({
          msg: state.data
        });
      }
    });
  }
}

class Child_2_1 extends Component{
  state = {
    msg: 'start'
  };
    
  componentDidUpdate() {
    console.log('Child_2_1 update', store.getState());
  }


  componentDidMount() {
    store.subscribe(() => {
      let state = store.getState();
      if (state.type === 'child_2_1') {
        this.setState({
          msg: state.data
        });
      }
    });
  }

  render() {
    return <div>
      <p>child_2_1 component: {this.state.msg}</p>
    </div>
  }
}

在上面的例子中,我們將一個(gè)名為 reducer 的函數(shù)作為參數(shù),生成我們所需要的 store,reducer 接受兩個(gè)參數(shù),一個(gè)是存儲(chǔ)在 store 里面的 state,另一個(gè)是每一次調(diào)用 dispatch 所傳進(jìn)來(lái)的 action。reducer 的作用,就是對(duì) dispatch 傳進(jìn)來(lái)的 action 進(jìn)行處理,并將結(jié)果返回。而里面的 state 可以通過(guò) store 里面的 getState 方法進(jìn)行獲得,其結(jié)果與最后一次通過(guò) reducer 處理后的結(jié)果保持一致。

在 child_1 組件中,我們每隔 1s 通過(guò) store 的 dispatch 方法,向 store 傳入包含有 type 字段的 action,reducer 直接將 action 進(jìn)行返回。

而在 child_2 與 child_2_1 組件中,通過(guò) store 的 subscribe 方法,監(jiān)聽(tīng) store 的變化,觸發(fā) dispatch 后,所有通過(guò) subscribe 進(jìn)行監(jiān)聽(tīng)的函數(shù)都會(huì)作出相應(yīng),根據(jù)當(dāng)前通過(guò) store.getState() 獲取到的結(jié)果進(jìn)行處理,對(duì)當(dāng)前組件的 state 進(jìn)行設(shè)置。所以我們可以在控制臺(tái)上看到各個(gè)組件更新及存儲(chǔ)在 store 中 state 的情況:

在 Redux 中,store 的作用,與 MVC 中的 Model 類(lèi)似,可以將我們項(xiàng)目中的數(shù)據(jù)傳遞給 store,交給 store 進(jìn)行處理,并可以實(shí)時(shí)通過(guò) store.getState() 獲取到存儲(chǔ)在 store 中的數(shù)據(jù)。我們對(duì)上面例子的 reducer 及各個(gè)組件的 componentDidMount 做點(diǎn)小修改,看看 store 的這一個(gè)特性。

import {createStore} from 'redux'

function reducer(state = {}, action) {
  switch (action.type) {
    case 'child_2':
      state.child_2 = action.data + ' child_2';
      return state;
    case 'child_2_1':
      state.child_2_1 = action.data + ' child_2_1';
      return state;
    default:
      return state
  }
}

let store = createStore(reducer);

class Child_1 extends Component{
  componentDidMount() {
    setTimeout(() => {
      store.dispatch({
        type: 'child_2',
        data: 'hello'
      })
    }, 1000);

    setTimeout(() => {
      store.dispatch({
        type: 'child_2_1',
        data: 'bye'
      })
    }, 2000);
  }
}

class Child_2 extends Component{
  componentDidMount() {
    store.subscribe(() => {
      let state = store.getState();

      if (state.hasOwnProperty('child_2')) {
        this.setState({
          msg: state.child_2
        });
      }
    });
  }
}

class Child_2_1 extends Component{
  componentDidMount() {
    store.subscribe(() => {
      let state = store.getState();

      if (state.hasOwnProperty('child_2_1')) {
        this.setState({
          msg: state.child_2_1
        });
      }
    });
  }
}

我們對(duì)創(chuàng)建 store 時(shí)所傳進(jìn)去的 reducer 進(jìn)行修改。reducer 中,其參數(shù) state 為當(dāng)前 store 的值,我們對(duì)不同的 action 進(jìn)行處理,并將處理后的結(jié)果存儲(chǔ)在 state 中并進(jìn)行返回。此時(shí),通過(guò) store.getState() 獲取到的,就是我們處理完成后的 state。

Redux 內(nèi)部的實(shí)現(xiàn),其實(shí)也是基于觀察者模式的,reducer 的調(diào)用結(jié)果,存儲(chǔ)在 store 內(nèi)部的 state 中,并在每一次 reducer 的調(diào)用中并作為參數(shù)傳入。所以在 child_1 組件第 2s 的 dispatch 后,child_2 與 child_2_1 組件通過(guò) subscribe 監(jiān)聽(tīng)的函數(shù),其通過(guò) getState 獲得的值,都包含有 child_2 與 child_2_1 字段的,這就是為什么第 2s 后的響應(yīng),child_2 也進(jìn)行了一次生命周期。所以在對(duì) subscribe 響應(yīng)后的處理,最好還是先校對(duì)通過(guò) getState() 獲取到的 state 與當(dāng)前組件的 state 是否相同。

// child_2
 componentDidMount() {
    store.subscribe(() => {
      let state = store.getState();

      if (state.hasOwnProperty('child_2')
        && state.child_2 !== this.state.msg) {
        this.setState({
          msg: state.child_2
        });
      }
    });
  }

加上這樣的校驗(yàn),各個(gè)組件的生命周期的觸發(fā)就符合我們的預(yù)期了。

Redux 對(duì)于組件間的解耦提供了很大的便利,如果你在考慮該不該使用 Redux 的時(shí)候,社區(qū)里有一句話說(shuō),“當(dāng)你不知道該不該使用 Redux 的時(shí)候,那就是不需要的”。Redux 用起來(lái)一時(shí)爽,重構(gòu)或者將項(xiàng)目留給后人的時(shí)候,就是個(gè)大坑,Redux 中的 dispatch 和 subscribe 方法遍布代碼的每一個(gè)角落。剛剛的例子不是最好的,F(xiàn)lux 設(shè)計(jì)中的 Controller-Views 概念就是為了解決這個(gè)問(wèn)題出發(fā)的,將所有的 subscribe 都置于 Parent 組件(Controller-Views),由最上層組件控制下層組件的表現(xiàn),然而,這不就是我們所說(shuō)的 子組件向父組件通訊 這種方式了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 今天來(lái)看一下react組件之間是怎么進(jìn)行通訊的。react推崇的是單向數(shù)據(jù)流,自上而下進(jìn)行數(shù)據(jù)的傳遞,但是由下而上...
    親親qin閱讀 6,077評(píng)論 2 12
  • 做React需要會(huì)什么? react的功能其實(shí)很單一,主要負(fù)責(zé)渲染的功能,現(xiàn)有的框架,比如angular是一個(gè)大而...
    蒼都閱讀 14,958評(píng)論 1 139
  • 學(xué)習(xí)必備要點(diǎn): 首先弄明白,Redux在使用React開(kāi)發(fā)應(yīng)用時(shí),起到什么作用——狀態(tài)集中管理 弄清楚Redux是...
    賀賀v5閱讀 9,069評(píng)論 10 58
  • 晚安,心 晚安,你們 晚安,螢火蟲(chóng) 晚安,星辰大海 晚安,街角的喧囂 晚安,還未安歇的你 晚安,不斷循環(huán)的回憶 晚...
    boyzcl閱讀 317評(píng)論 0 2
  • 2018年目標(biāo)如下: 1貴州自駕遊 2最少一到三個(gè)月休息 3學(xué)瑜伽 4自學(xué)鋼琴,起碼會(huì)彈幾曲拿手的。 5雲(yún)之南端....
    wenwen7446閱讀 150評(píng)論 0 0

友情鏈接更多精彩內(nèi)容