Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React组件之间的通信 #27

Open
sunyongjian opened this issue Jul 28, 2017 · 3 comments
Open

React组件之间的通信 #27

sunyongjian opened this issue Jul 28, 2017 · 3 comments
Labels

Comments

@sunyongjian
Copy link
Owner

sunyongjian commented Jul 28, 2017

引入

本来我是没想过总结这些东西的,会感觉太入门了。但是之前同学去腾讯面试问到了这个问题(react或vue的组件通信),我帮他整理,顺便写demo的过程中,会有一些新的体会,多总结还是有利于进步的呀。

另外本次的代码都放在 https://github.com/sunyongjian/rc-communication-demo , 可以 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 的传递。另外:这个功能是实验性的,未来可能会有所改动。

Context 也存在自己的问题。如果你用过 context,你可能会发现一个问题,当 context 发生改变的时候,比如数据流向是从 Container(context 定义) -> A -> B -> C(接收 context),组件 A, B 也会发生 render,这样 C 组件才能拿到更新后的 context。万一你在 A, B 使用 shouldComponentUpdate: false 拦截了,或者某个组件是 PureComponent,context 发生变化,C 没有重新渲染,故拿不到最新的 context。

针对这种情况,我们要做的不是想方设法让 A,B render,而是通过其他手段,来实现 C 的重新渲染。通常是使用 context 做依赖注入,即 context 只注入一次,后续不会发生变化,这样各种无视组件层级透传属性。context 里面的数据进行改造,添加 subscribe 这样的函数,然后当某个数据变化的时候做 patch。子组件可能会加这样的代码:

// 子组件
  componentDidMount() {
    this.context.theme.subscribe(() => this.forceUpdate())
  }

这种思想可以安全的使用 context,事实上 react-redux 也是这样做的。Provider 提供 context,connect 去做订阅。

关于此推荐一篇文章How to safely use React context,需翻墙

发布订阅

这种一个地方发送消息,另一个地方接收做出变化的需求,很容易想到的就是观察者模式了。具体的实现会有很多种,这里我们自己写了一个 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 的定义,还是看具体的应用场景了。

@madneal
Copy link

madneal commented Mar 10, 2018

如果想和兄弟组件的子组件通信的话 是不是只能自己设置事件订阅 或者第三方状态管理?

@sunyongjian
Copy link
Owner Author

@neal1991 不是啊。你可以状态提升,或者使用 context,或者用 订阅,redux 等都可以。比如组件层级是
A1 包含 B1 和 B2,B 分别是 C1 和 C2。如果想要 C1 和 C2 通信,你可以把 state 定义到 A 中,这属于变量提升,风险就是如果再有和 A1 同层级的 A2 ,下面一个组件想和 C1 通信,state 可能就要重新改动,提升到 A1 和 A2 的 container。另外 Context 也是可以的啊,只不过层级太深会有一定的风险,我们通常用它作注入(provider)的一种方式 ,而不是 state 变化做通知的。所以还是要结合场景的啊,直接上状态管理库可能不用想太多,很少出现一些项目复杂后的问题,但是有时候是不必要的。

@madneal
Copy link

madneal commented Mar 11, 2018

@sunyongjian 感谢回复

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants