react 组件通信

3,099 阅读4分钟

React 组件通信

前沿

当一个项目使用 React 作为项目的框架时,我们就可以将项目(业务)划分成一个或多个组件。它的优点在于项目在后期的维护中更加清晰和方便。

因为 React 组件化的缘故,一个完整的项目会有一个或多个、相同或不同的组件拼接组成的(我们可以脑补一下,类似搭积木的形式。把它们理解为 HTMLDOM 结构更合适)。

那么这就使得组件之间的通信十分的重要。本文就来介绍 React 如何进行组件通信

什么是 props

当我们使用 React 时,使用的是 JSX 语法。(webpack 在打包编译的时候使用 babel-loader 转译成 ES5 的语法。目前浏览器使用的都是 ES5 语法)。

在使用自定义组件的时候, JSX 会将所接受到的属性以及子组件(children)转化为单个对象的属性传递给组件,这个对象被称之为 props

什么是 React 单向数据流

React 中,数据仅朝一个方向进行传递,即从父组件流向子组件。如果数据在兄弟子组件之间共享,那么数据应该存储在父组件(state),并把数据传递给需要的其他子组件。

数据如何更新

数据只有存储在父组件(容器组件)的 state 对象中,并通过 this.setState() 方法更新对应的数据,那么容器组件和引用了该数据的子组件都会进行更新。

React 数据更新的方式

  • 如果必须更新数据的话,则只有父组件应该进行更新;
  • 如果子组件需要更改数据,它必须以回调函数的形式通知父组件,由父组件完成更新。

组件通信有哪几种形式

  • 父组件向子组件通信
  • 子组件向父组件通信
  • 跨级组件通信
  • 兄弟组件通信

父组件向子组件通信

这是最简单也是最常用的一种通信方式,父组件向子组件传递 props,子组件得到 props 后进行相应的处理。

  function Sup(props) {
    const { user } = props;
    return (
      <p className="sup-context">
        <span className="sup-context-title">Who am I, you are</span>
        : {user}
      </p>
    );
  }
export default class Container extends Component {
  constructor() {
    super();
    this.state = {
      userName: 'shenxuxiang',
    };
  }

  render() {
    return (
      <div className="page-one">
        <h1 className="page-one-title">父组件向子组件通信</h1>
        <Sup user={this.state.userName} />
      </div>
    );
  }
}

Container 是一个父组件,Sup 作为子组件。我们将 user 属性传递到了 Sup 内部。这时我们就可以发现,在 Sup 内部是可以拿到 user 的值的。

数据更新

当父组件(容器组件)的 state 数据更新的时候,那么对于那些使用了该数据的子组件会进行重新渲染并拿到更新后的数据。

export default class Container extends Component {
  constructor() {
    super();
    this.state = {
      userName: 'shenxuxiang',
    };
  }

  handleClick = (event) => {
    const { target } = event;
    this.setState({ userName: target.getAttribute('data-name') });
  }

  render() {
    return (
      <div className="page-one">
        <h1 className="page-one-title">父组件向子组件通信</h1>
        <ul onClick={this.handleClick} className="page-one-user-list">
          <li className="page-one-user-list-item" data-name="小明">小明</li>
          <li className="page-one-user-list-item" data-name="小强">小强</li>
          <li className="page-one-user-list-item" data-name="小李">小李</li>
        </ul>
        <Sup user={this.state.userName} />
      </div>
    );
  }
}

page-one-user-list 节点发生点击事件的时候,会在事件处理函数中调用 this.setState() ,更新相应的数据。

子组件向父组件通信

利用回调函数,可以实现子组件向父组件通信:父组件将一个函数作为 props 传递给子组件,子组件调用该回调函数,便可以向父组件通信。

export default class Sup extends PureComponent {
  handleClick = (event) => {
    const name = event.target.getAttribute('data-name');
    // 这种直接修改 props 的形式会是页面报错
    // this.props.user = name;
    this.props.onChange(name);
  }

  render() {
    return (
      <Fragment>
        <p className="page-two-sup-context">
          <span className="page-two-sup-context-title">Who am I, you are</span>
          : {this.props.user}
        </p>
        <ul onClick={this.handleClick} className="page-two-user-list">
          <li className="page-one-user-list-item" data-name="小明">小明</li>
          <li className="page-one-user-list-item" data-name="小强">小强</li>
          <li className="page-one-user-list-item" data-name="小李">小李</li>
        </ul>
      </Fragment>
    );
  }
}
export default class Container extends Component {
  constructor() {
    super();
    this.state = {
      userName: 'shenxuxiang',
    };
  }

  handleChangeUserName = (name) => {
    this.setState({ userName: name });
  }

  render() {
    return (
      <div className="page-two">
        <h1 className="page-two-title">子组件向父组件通信</h1>
        <Sup user={this.state.userName} onChange={this.handleChangeUserName} />
      </div>
    );
  }
}

跨级组件之间通信

跨级组件通信,就是父组件向子组件的子组件通信,一般嵌套的层级超过三层我们就可以称为跨级。跨级组件通信可以采用下面两种方式:

  • 中间组件层层传递 props
  • 使用 context 对象

中间组件层层传递

如果父组件结构较深,那么中间的每一层组件都要去传递 props,增加了复杂度,有时这些 props 并不是这些中间组件自己所需要的。

function Sup(props) {
  return (
    <div className="page-three-sup">
      <div>这是 sup</div>
      <Sub user={props.user}/>
    </div>
  );
}
function Sub(props) {
  return (
    <div className="page-three-sub">
      <div>这是 sub</div>
      <div>{`props.user = ${props.user}`}</div>
    </div>
  );
}

class Container extends Component {
  constructor() {
    super();
    this.state = {
      userName: 'shenxuxiang',
    };
  }
  render() {
    return (
      <div className="page-three">
        <h1 className="page-three-title">跨级组件通信</h1>
        <Sup user={this.state.userName} />
      </div>
    );
  }
}

使用 context 对象

使用 context 我们可以把要通信的内容放在这个容器中,这样一来,不管嵌套有多深,都可以随意取用。

创建一个 context 对象

  const ThemContext = React.createContext('skyblue');

必须使用 React.createContext 方法进行创建,参数可以是任意类型的值

Context.Provider

  <ThemContext.Provider value={this.state.themColor}>
    <Sup />
  </ThemContext.Provider>

每个 Context 对象都会返回一个 Provider 组件,它允许消费组件订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

Providervalue 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数。

Context 赋值给组件的静态属性 contextType

  Container.contextType = ThemContext;

这能让你使用 this.context 来消费最近 Context 上的那个值

完整代码

const ThemContext = React.createContext('skyblue');
export default class Container extends Component {
  constructor() {
    super();
    this.state = {
      themColor: '#f80',
    };
  }
  render() {
    return (
      <div className="page-three">
        <h1 className="page-three-title">跨级组件通信</h1>
        <ThemContext.Provider value={this.state.themColor}>
          <Sup />
        </ThemContext.Provider>
      </div>
    );
  }
}
Container.contextType = ThemContext;
function Sup(props) {
  return (
    <div className="page-three-sup">
      <div>这是 sup</div>
      <Sub />
    </div>
  );
}
function Sub(props) {
  const context = useContext(ThemContext);
  return (
    <div
      className="page-three-sub"
      style={{ background: context }}
    >
      <div>这是 sub</div>
    </div>
  );
}

如何更新 context

context 数据更新同 props 一样,只能在容器组件中进行更新。同样的,如果某个子组件想要去修改context。那么就必须通过给 context 传递一个函数,然后在子组件中调用这个函数,方才可以更新 context

const ThemContext = React.createContext({
  color: 'skyblue',
  dispatch: () => {},
});

export default class Container extends Component {
  static contextType = ThemContext;
  constructor() {
    super();
    this.state = {
      color: '#f80',
    };
  }
  handleChangeThem = (color) => {
    this.setState({ color });
  }
  render() {
    return (
      <div className="page-three">
        <h1 className="page-three-title">跨级组件通信</h1>
        <ThemContext.Provider
          value={{ color: this.state.color, dispatch: this.handleChangeThem }}
        >
          <Sup />
        </ThemContext.Provider>
      </div>
    );
  }
}

function Sup(props) {
  return (
    <div className="page-three-sup">
      <h1>这是 sup</h1>
      <Sub />
    </div>
  );
}
class Sub extends Component {
  handleClick = (event) => {
    const color = event.target.getAttribute('data-color');
    this.context.dispatch(color);
  }
  render() {
    console.log(this.context);
    return (
      <div 
        className="page-three-sub"
        style={{ background: this.context.color }}
      >
        <h2>这是 sub:</h2>
        <ul onClick={this.handleClick} className="page-three-user-list">
          <li className="page-three-user-list-item" data-color="white">white</li>
          <li className="page-three-user-list-item" data-color="skyblue">skyblue</li>
          <li className="page-three-user-list-item" data-color="green">green</li>
        </ul>
      </div>
    );
  }
}
Sub.contextType = ThemContext;

兄弟组件通信

兄弟组件通信,指的是在同一个父组件中的两个及以上的组件之间如何进行通信。针对这种情况,一般的做法就是将数据来源存放在它们共同的父级组件中。

export default class Container extends Component {
  constructor() {
    super();
    this.state = {
      userName: 'shenxuxiang',
    };
  }
  handleClick = (name) => {
    this.setState({ userName: name });
  }
  render() {
    const { userName } = this.state;
    return (
      <div className="page-four">
        <h1 className="page-four-title">兄弟组件通信</h1>
        <h2>userName={userName}</h2>
        <Foo user={userName} onClick={this.handleClick} />
        <Bar user={userName} onClick={this.handleClick} />
      </div>
    );
  }
}

function Bar(props) {
  return (
    <div onClick={() => props.onClick('bar')}>
      我是组件 Bar。{`【${props.user}】`}
    </div>
  );
}
function Foo(props) {
  return (
    <div onClick={() => props.onClick('foo')}>
      我是组件 Foo。{`【${props.user}】`}
    </div>
  );
}

案例中组件 Foo、Bar 之间的通信只是众多形式中的一种,他可以很简单,也可以很复杂。有时,当 props 这种形式无法满足我们的需求时,可以配合 context 一起使用,从而达到我们的目的。

总结

本文所介绍的四种组件通信方式,基本上已经涵盖了项目中大部分场景。方式是固定的,我们在项目中可以根据具体的业务需求进行调整。当然除了 react 提供的这几种形式化,javascript 的【发布订阅】模式,也能够实现组件之间的通信。当然,我仍然推荐使用 react 自身提供的方法。