1. 程式人生 > >React躬行記(9)——元件通訊

React躬行記(9)——元件通訊

  根據元件之間的巢狀關係(即層級關係)可分為4種通訊方式:父子、兄弟、跨級和無級。

一、父子通訊

  在React中,資料是自頂向下單向流動的,而父元件通過props向子元件傳遞需要的資訊是元件之間最常見的通訊方式,如下程式碼所示,父元件Parent向子元件Child傳遞了一個name屬性,其值為一段字串“strick”。

class Parent extends React.Component {
  render() {
    return <Child name="strick">子元件</Child>;
  }
}
class Child extends React.Component {
  render() {
    return <input name={this.props.name} type="text" />;
  }
}

  當需要子元件向父元件傳遞資訊時,也能通過元件的props實現,只是要多傳一個回撥函式,如下所示。

class Parent extends React.Component {
  callback(value) {
    console.log(value);        //輸出從子元件傳遞過來的值
  }
  render() {
    return <Child callback={this.callback} />;
  }
}
class Child extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: "" };
  }
  handle(e) {
    this.props.callback(e.target.value);        //呼叫父元件的回撥函式
    this.setState({ name: e.target.value });    //更新文字框中的值
  }
  render() {
    return <input value={this.state.name} type="text" onChange={this.handle.bind(this)} />;
  }
}

  父元件Parent會傳給子元件Child一個callback()方法,子元件中的文字框註冊了一個onChange事件,在事件處理程式handle()中將回調父元件的callback()方法,並把文字框的值傳遞過去,以此達到反向通訊的效果。

二、兄弟通訊

  當兩個元件擁有共同的父元件時,就稱它們為兄弟元件,注意,它們可以不在一個層級上,如圖6所示,C與D或E都是兄弟關係。

圖6  元件樹

  兄弟之間不能直接通訊,需要藉助狀態提升的方式間接實現資訊的傳遞,即把元件之間要共享的狀態提升至最近的父元件中,由父元件來統一管理。而任意一個兄弟元件可通過從父元件傳來的回撥函式更新共享狀態,新的共享狀態再通過父元件的props回傳給子元件,從而完成一次兄弟之間的通訊。在下面的例子中,會有兩個文字框(如圖7所示),當向其中一個輸入數字時,鄰近的文字框會隨之改變,要麼加一,要麼減一。

圖7  兩個文字框

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { type: "p", digit: 0 };
    this.plus = this.plus.bind(this);
    this.minus = this.minus.bind(this);
  }
  plus(digit) {
    this.setState({ type: "p", digit });
  }
  minus(digit) {
    this.setState({ type: "m", digit });
  }
  render() {
    let { type, digit } = this.state;
    let pdigit = type == "p" ? digit : (digit+1);
    let mdigit = type == "m" ? digit : (digit-1);
    return (
      <>
        <Child type="p" digit={pdigit} onDigitChange={this.plus} />
        <Child type="m" digit={mdigit} onDigitChange={this.minus} />
      </>
    );
  }
}
class Child extends React.Component {
  constructor(props) {
    super(props);
    this.handle = this.handle.bind(this);
  }
  handle(e) {
    this.props.onDigitChange(+e.target.value);
  }
  render() {
    return (
      <input value={this.props.digit} type="text" onChange={this.handle} />
    );
  }
}

  上面程式碼實現了一次完整的兄弟之間的通訊,具體過程如下所列。

(1)首先在父元件Parent中定義兩個兄弟元件Child,其中type屬性為“p”的子元件用於遞增,綁定了plus()方法;type屬性為“m”的子元件用於遞減,綁定了minus()方法。

(2)然後在子元件Child中接收傳遞過來的digit屬性和onDigitChange()方法,前者會作為文字框的值,後者會在事件處理程式onChange()中被呼叫。

(3)如果在遞增文字框中修改數值,那麼就將新值傳給plus()方法。遞減文字框的處理過程與之類似,只是將plus()方法替換成minus()方法。

(4)最後更新父元件中的兩個狀態:type和digit,完成資訊的傳遞。

三、跨級通訊

  在一棵元件樹中,當多個元件需要跨級通訊時,所處的層級越深,那麼需要過渡的中間層就越多,完成一次通訊將變得非常繁瑣,而在資料傳遞過程中那些作為橋樑的元件,其程式碼也將變得冗餘且臃腫。

  在React中,還可用Context實現跨級通訊。Context能存放元件樹中需要全域性共享的資料,也就是說,一個元件可以藉助Context跨越層級直接將資料傳遞給它的後代元件。如圖8所示,左邊的資料會通過元件的props逐級顯式地傳遞,右邊的資料會通過Context讓所有元件都可訪問。

 圖8  props和context

   隨著React v16.3的釋出,引入了一種全新的Context,修正了舊版本中較為棘手的問題,接下來的篇幅將著重分析這兩個版本的Context。

1)舊的Context

   在舊版本的Context中,首先要在頂層元件內新增getChildContext()方法和靜態屬性childContextTypes,前者用於生成一個context物件(即初始化Context需要攜帶的資料),後者通過prop-types庫限制該物件的屬性的資料型別,兩者缺一不可。在下面的示例中,Grandpa是頂層元件,Son是中間元件,要傳遞的是一個包含name屬性的物件。

//頂層元件
class Grandpa extends React.Component {
  getChildContext() {
    return { name: "strick" };
  }
  render() {
    return <Son />;
  }
}
Grandpa.childContextTypes = {
  name: PropTypes.string
};
//中間元件
class Son extends React.Component {
  render() {
    return <Grandson />;
  }
}

  然後給後代元件(例如下面的Grandson)新增靜態屬性contextTypes,限制要接收的屬性的資料型別,最後就能通過讀取this.context得到由頂層元件提供的資料。

class Grandson extends React.Component {
  render() {
    return <p>{this.context.name}</p>;
  }
}
Grandson.contextTypes = {
  name: PropTypes.string
};

  從上面的示例中可以看出,跨級通訊的準備工作並不簡單,需要在兩處做不同的配置。React官方建議慎用舊版的Context,因為它相當於JavaScript中的全域性變數,容易造成資料流混亂、重名覆蓋等各種副作用,並且在未來的React版本中有可能被廢棄。

  雖然在功能上Context實現了跨級通訊,但本質上資料還是像props一樣逐級傳遞的,因此如果某個中間元件的shouldComponentUpdate()方法返回false的話,就會阻止下層的元件更新Context中的資料。接下來會演示這個致命的缺陷,沿用上一個示例,對兩個元件做些調整。在Grandpa元件中,先讓Context儲存元件的name狀態,再新增一個按鈕,併為其註冊一個能更新元件狀態的點選事件;在Son元件中,新增shouldComponentUpdate()方法,它的返回值是false。在把Grandpa元件掛載到DOM中後,點選按鈕就能發現Context的更新傳播終止於Son元件。

class Grandpa extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: "strick" };
    this.click = this.click.bind(this);
  }
  getChildContext() {
    return { name: this.state.name };
  }
  click() {
    this.setState({ name: "freedom" });
  }
  render() {
    return (
      <>
        <Son />
        <button onClick={this.click}>提交</button>
      </>
    );
  }
}
class Son extends React.Component {
  shouldComponentUpdate() {
    return false;
  }
  render() {
    return <Grandson />;
  }
}

2)新的Context

  這個版本的Context不僅採用了更符合React風格的宣告式寫法,還可以直接將資料傳遞給後代元件而不用逐級傳遞,一舉衝破了shouldComponentUpdate()方法的限制。下面仍然使用上一節的三個元件,完成一次新的跨級通訊。

const NameContext = React.createContext({name: "strick"});
class Grandpa extends React.Component {
  render() {
    return (
        <NameContext.Provider value={{name: "freedom"}}>
          <Son />
        </NameContext.Provider>
    );
  }
}
class Son extends React.Component {
  render() {
    return <Grandson />;
  }
}
class Grandson extends React.Component {
  render() {
    return (
        <NameContext.Consumer>{context => <p>{context.name}</p>}</NameContext.Consumer>
    );
  }
}

  通過上述程式碼可知,新的Context由三部分組成:

(1)React.createContext()方法,接收一個可選的defaultValue引數,返回一個Context物件(例如NameContext),包含兩個屬性:Provider和Consumer,它們是一對相呼應的元件。

(2)Provider,來源元件,它的value屬性就是要傳送的資料,Provider可關聯多個來自於同一個Context物件的Consumer,像NameContext.Provider只能與NameContext.Consumer配合使用。

(3)Consumer,目標元件,出現在Provider之後,可接收一個返回React元素的函式,如果Consumer能找到對應的Provider,那麼函式的引數就是Provider的value屬性,否則就讀取defaultValue的值。

  注意,Provider元件會通過Object.is()對其value屬性的新舊值做比較,以此確定是否更新作為它後代的Consumer元件。

四、無級通訊

  當兩個沒有巢狀關係(即無級)的元件需要通訊時,可以藉助訊息佇列實現。下面是一個用觀察者模式實現的簡易訊息佇列庫,其處理過程類似於事件系統,如果將訊息看成事件,那麼訂閱訊息就是繫結事件,而釋出訊息就是觸發事件。

class EventEmitter {
  constructor() {
    this.events = {};
  }
  sub(event, listener) {        //訂閱訊息
    if (!this.events[event]) {
      this.events[event] = { listeners: [] };
    }
    this.events[event].listeners.push(listener);
  }
  pub(name, ...params) {        //釋出訊息
    for (const listener of this.events[name].listeners) {
      listener.apply(this, params);
    }
  }
}

  EventEmitter只包含了三個方法,它們的功能如下所列:

(1)建構函式,初始化了一個用於快取各類訊息的容器。

(2)sub()方法,將回調函式用訊息名稱分類儲存。

(3)pub()方法,依次執行了指定名稱下的訊息集合。

  下面用一個示例演示無級通訊,在Sub元件的建構函式中,會訂閱一次訊息,訊息名稱為"TextBox",回撥函式會接收一個引數,並將其輸出到控制檯。

let emitter = new EventEmitter();
class Sub extends React.Component {
  constructor(props) {
    super(props);
    emitter.sub("TextBox", value => console.log(value));
  }
  render() {
    return <p>訂閱訊息</p>;
  }
}

  在下面的Pub元件中,為文字框註冊了onChange事件,在事件處理程式handle()中釋出名為"TextBox"的訊息集合,並將文字框中的值作為引數傳遞到回撥函式中。

class Pub extends React.Component {
  constructor(props) {
    super(props);
    this.state = { value: "" };
  }
  handle(e) {
    const value = e.target.value;
    emitter.pub("TextBox", value);
    this.setState({ value });
  }
  render() {
    return <input value={this.state.value} onChange={this.handle.bind(this)} />;
  }
}

  Sub元件和Pub元件會像下面這樣,以兄弟的關係掛載到DOM中。當修改文字框中的內容時,就會觸發訊息的釋出,從而完成了一次它們之間的通訊。

ReactDOM.render(
  <>
    <Sub />
    <Pub />
  </>,
  document.getElementById("container")
);

  當業務邏輯複雜到一定程度時,普通的訊息佇列可能就捉襟見肘了,此時可以考慮引入Mobx、Redux等專門的狀態管理工具來實現元件之間的通訊。

&n