React.js 中的元件通訊問題

sunyongjian發表於2017-07-31

引入

本來我是沒想過總結這些東西的,會感覺比較入門。但是之前同學去騰訊面試問到了這個問題(react或vue的元件通訊),我幫他整理,順便寫demo的過程中,會有一些新的體會,多總結還是有利於進步的呀。

另外本次的程式碼都放在 github.com/sunyongjian… , 可以 done 下來加深理解。

父子元件

父 → 子

parent元件傳給child元件,符合react的單向資料流理念,自上到下傳遞props。

// 父元件
class Parent extends Component {
  constructor() {
    super();
    this.state = {
      value: '',
    }
  }

  handleChange = e => {
    this.value = e.target.value;
  }

  handleClick = () => {
    this.setState({
      value: this.value,
    })
  }

  render() {
    return (
      <div>
        我是parent
        <input onChange={this.handleChange} />
        <div className="button" onClick={this.handleClick}>通知</div>
        <div>
          <Child value={this.state.value} />
        </div> 
      </div>
    );
  }
}複製程式碼
// 子元件
class Child extends Component {
  render() {
    const { value } = this.props;
    return (
      <div>
        我是Child,得到傳下來的值:{value}
      </div>
    );
  }
}複製程式碼

父元件做的就是定義好 state ,定義好事件函式,input onChange 的時候,去快取 value 值,然後點選 button 的時候,改變 state , 子元件只負責展示 value 。

子 → 父

child 元件通知 parent 元件, 主要是依靠 parent 傳下來的 callback 函式執行,改變 parent 元件的狀態,或者把 child 自己的 state 通知 parent 。分兩種情況:

  • state 定義在 parent 元件
// parent

class Parent extends Component {
  constructor() {
    super();
    this.state = {
      value: '',
    }
  }

  setValue = value => {
    this.setState({
      value,
    })
  }

  render() {
    return (
      <div>
        <div>我是parent, Value是:{this.state.value}</div> 
        <Child setValue={this.setValue} />
      </div>
    );
  }
}複製程式碼

class Child extends Component {

  handleChange = e => {
    this.value = e.target.value;
  }

  handleClick = () => {
    const { setValue } = this.props;
    setValue(this.value);
  }

  render() {
    return (
      <div>
        我是Child
        <div className="card">
          state 定義在 parent
          <input onChange={this.handleChange} />
          <div className="button" onClick={this.handleClick}>通知</div>
        </div>
      </div>
    );
  }
}複製程式碼

parent 元件把改變 state 的 setValue 函式傳給 child ,child 元件自己處理內部的狀態(這裡是表單的value值),當 child 元件分發訊息的時候, 執行 parent 的 setValue 函式,從而改變了 parent 的 state,state發生變化, parent 元件執行 re-render 。

  • state 定義在 child 元件
// parent

class Parent extends Component {

  onChange = value => {
    console.log(value, '來自 child 的 value 變化');
  }

  render() {
    return (
      <div>
        <div>我是parent
        <Child onChange={this.onChange} />
      </div>
    );
  }
}複製程式碼

class Child extends Component {

  constructor() {
    super();
    this.state = {
      childValue: ''
    }
  }

  childValChange = e => {
    this.childVal = e.target.value;
  }

  childValDispatch = () => {
    const { onChange } = this.props;
    this.setState({
      childValue: this.childVal,
    }, () => { onChange(this.state.childValue) })
  }

  render() {
    return (
      <div>
        我是Child
        <div className="card">
          state 定義在 child
          <input onChange={this.childValChange} />
          <div className="button" onClick={this.childValDispatch}>通知</div>
        </div>
      </div>
    );
  }
}複製程式碼

有時候 state 是需要定義在 child 元件的,比如彈窗, CheckBox 這種開關性質的,邏輯是重複的,state 定義在元件內部更好維護, 複用性更好。但是 child 的 state 是需要告知我的 parent 元件的, 同樣還是執行 parent 傳下來的 change 函式。

兄弟元件

有時候可能出現頁面中的某兩部分通訊,比如省市的級聯選擇,點選 button 改變顏色等等,元件並不是父子級,沒有巢狀關係的時候。這種時候通常是依賴共有的頂級 Container 處理或者第三方的狀態管理器。其實原理都是相通的,兄弟 A 的 value 發生變化,分發的時候把 value 值告訴一箇中間者 C ,C 會自動告知 B,實現 B 的自動render 。

利用共有的Container

// container
class Container extends Component {
  constructor() {
    super();
    this.state = {
      value: '',
    }
  }

  setValue = value => {
    this.setState({
      value,
    })
  }

  render() {
    return (
      <div>
        <A setValue={this.setValue}/>
        <B value={this.state.value} />
      </div>
    );
  }
}複製程式碼
// 兄弟A
class A extends Component {

  handleChange = (e) => {
    this.value = e.target.value;
  }

  handleClick = () => {
    const { setValue } = this.props;
    setValue(this.value);
  }

  render() {
    return (
      <div className="card">
        我是Brother A, <input onChange={this.handleChange} />
        <div className="button" onClick={this.handleClick}>通知</div>
      </div>
    )
  }
}複製程式碼
// 兄弟B
const B = props => (
  <div className="card">
    我是Brother B, value是:
    {props.value}
  </div>
);
export default B;複製程式碼

元件 A 中的表單 value 值,告知了父級 Container 元件(通過 setValue 函式改變 state),元件 B 依賴於 Container 傳下來的 state,會做出同步更新。這裡的中間者是 Container。

利用Context

上面的方式,如果巢狀少還可以,如果巢狀特別多,比如一級導航欄下的二級導航欄下的某個按鈕,要改變頁面中 content 區域的 table 裡的某個列的值...他們同屬於一個 page 。這樣傳遞 props 就會很痛苦,每一層元件都要傳遞一次。

// 頂級公共元件
class Context extends Component {

  constructor() {
    super();
    this.state = {
      value: '',
    };
  }

  setValue = value => {
    this.setState({
      value,
    })
  }

  getChildContext() { // 必需
    return { 
      value: this.state.value,
      setValue: this.setValue,
    };
  }
  render() {
    return (
      <div>
        <AParent />
        <BParent />
      </div>
    );
  }
}
// 必需
Context.childContextTypes = {
  value: PropTypes.string,
  setValue: PropTypes.func,
};複製程式碼
// A 的 parent
class AParent extends Component {
  render() {
    return (
      <div className="card">
        <A />
      </div>
    );
  }
}
// A
class A extends Component {

  handleChange = (e) => {
    this.value = e.target.value;
  }

  handleClick = () => {
    const { setValue } = this.context;
    setValue(this.value);
  }

  render() {
    return (
      <div>
        我是parentA 下的 A, <input onChange={this.handleChange} />
        <div className="button" onClick={this.handleClick}>通知</div>
      </div>
    );
  }
}
// 必需
A.contextTypes = {
  setValue: PropTypes.func,
};複製程式碼
// B 的 parent
class BParent extends Component {
  render() {
    return (
      <div className="card">
        <B />
      </div>
    );
  }
}

// B
class B extends Component {

  render() {
    return (
      <div>
        我是parentB 下的 B, value是:
        {this.context.value}
      </div>
    );
  }
}

B.contextTypes = {
  value: PropTypes.string,
};複製程式碼

元件 A 仍是 訊息的傳送者,元件 B 是接收者, 中間者是 Context 公有 Container 元件。context是官方文件的一個 API ,通過 getChildContext 函式定義 context 中的值,並且還要求 childContextTypes 是必需的。這樣屬於這個 Container 元件的子元件,通過 this.context 就可以取到定義的值,並且起到跟 state 同樣的效果。中間者其實還是 Container,只不過利用了上下文這樣的 API ,省去了 props 的傳遞。另外:這個功能是實驗性的,未來可能會有所改動。

釋出訂閱

這種一個地方傳送訊息,另一個地方接收做出變化的需求,很容易想到的就是觀察者模式了。具體的實現會有很多種,這裡我們自己寫了一個 EventEmitter 的類(其實就是仿照 node 中的 EventEmitter 類),如果不瞭解觀察者,可以看我的另一篇文章觀察者模式

// 釋出訂閱類
class EventEmitter {
  _event = {}

  // on 函式用於繫結
  on(eventName, handle) {
    let listeners = this._event[eventName];
    if(!listeners || !listeners.length) {
      this._event[eventName] = [handle];
      return;
    }
    listeners.push(handle);
  }
  // off 用於移除
  off(eventName, handle) {
    let listeners = this._event[eventName];
    this._event[eventName] = listeners.filter(l => l !== handle);
  }
  // emit 用於分發訊息
  emit(eventName, ...args) {
    const listeners = this._event[eventName];
    if(listeners && listeners.length) {
      for(const l of listeners) {
        l(...args);
      }
    }
  }
}
const event = new EventEmitter;
export { event };複製程式碼
// Container
import A from './a';
import B from './b';

const Listener = () => {
  return (
    <div>
      <A />
      <B />
    </div>
  );
};
export default Listener;複製程式碼
// 兄弟元件 A
import { event } from './eventEmitter';

class A extends Component {

  handleChange = e => {
    this.value = e.target.value;
  }

  handleClick = () => {
    event.emit('dispatch', this.value);
  }

  render() {
    return (
      <div className="card">
        我是Brother A, <input onChange={this.handleChange} />
        <div className="button" onClick={this.handleClick}>通知</div>
      </div>
    )
  }
}複製程式碼
// 兄弟元件 B
import { event } from './eventEmitter';

class B extends Component {
  state = {
    value: ''
  }

  componentDidMount() {
    event.on('dispatch', this.valueChange);
  }

  componentWillUnmount() {
    event.off('dispatch', this.valueChange);
  }

  valueChange = value => {
    this.setState({
      value,
    })
  }

  render() {
    return (
      <div className="card">
        我是Brother B, value是:
        {this.state.value}
      </div>
    );
  }
}複製程式碼

仍然是元件 A 用於分發訊息,元件 B 去接收訊息。這裡的中間者其實就是 event 物件。需要接收訊息的 B 去訂閱 dispatch 事件,並把回撥函式 valueChange 傳入,另外 B 定義了自己的 state,方便得到 value 值的時候自動渲染。元件 A 其實就是把內部的表單 value 在點選的時候分發,釋出事件,從而 B 中的 valueChange 執行,改變 state。這種方式比較方便,也更直觀,不需要藉助 Container 元件去實現,省去了很多邏輯。

Redux || Mobx

Redux 或者 Mobx 是第三方的狀態管理器,是這裡我們通訊的中間者。大型專案最直接的就是上庫... 更方便,更不容易出錯。 但其實小專案就沒什麼必要了。東西比較多,這裡不再闡述它們的實現和做了什麼。

總結

react 特殊的自上而下的單向資料流,和 state 的特性,造就以這樣的思想實現元件通訊。除去釋出訂閱和 Redux 等,其他的都是 props 自上而下傳遞的理念,子元件需要的總是通過父元件傳遞下來的,關於 state 的定義,還是看具體的應用場景了。

PS: 個人blog github.com/sunyongjian…
歡迎Star

相關文章