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等专门的状态管理工具来实现组件之间的通讯。

相关文章
相关标签/搜索