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

  根據組件之間的嵌套關係(即層級關係)可分爲4種通訊方式:父子、兄弟、跨級和無級。html

1、父子通訊

  在React中,數據是自頂向下單向流動的,而父組件經過props向子組件傳遞須要的信息是組件之間最多見的通訊方式,以下代碼所示,父組件Parent向子組件Child傳遞了一個name屬性,其值爲一段字符串「strick」。git

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()方法,並把文本框的值傳遞過去,以此達到反向通訊的效果。app

2、兄弟通訊

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

圖6  組件樹ide

  兄弟之間不能直接通訊,須要藉助狀態提高的方式間接實現信息的傳遞,即把組件之間要共享的狀態提高至最近的父組件中,由父組件來統一管理。而任意一個兄弟組件可經過從父組件傳來的回調函數更新共享狀態,新的共享狀態再經過父組件的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} />
    );
  }
}

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

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

(2)而後在子組件Child中接收傳遞過來的digit屬性和onDigitChange()方法,前者會做爲文本框的值,後者會在事件處理程序onChange()中被調用。

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

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

3、跨級通訊

  在一棵組件樹中,當多個組件須要跨級通訊時,所處的層級越深,那麼須要過渡的中間層就越多,完成一次通訊將變得很是繁瑣,而在數據傳遞過程當中那些做爲橋樑的組件,其代碼也將變得冗餘且臃腫。

  在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組件。

4、無級通訊

  當兩個沒有嵌套關係(即無級)的組件須要通訊時,能夠藉助消息隊列實現。下面是一個用觀察者模式實現的簡易消息隊列庫,其處理過程相似於事件系統,若是將消息當作事件,那麼訂閱消息就是綁定事件,而發佈消息就是觸發事件。

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等專門的狀態管理工具來實現組件之間的通訊。

相關文章
相關標籤/搜索