react组件通信方式

使用React开发有一个多月了,在项目中主要是用厂里的组件库进行开发。除了部分需求的个性化定制之外,基本上能够满足日常需求,从开发效率上来说,使用组件库带来的提升还是相当明显的。不用太过于关注UI层面的实现细节,考虑最多的也就是组件与组件之间的数据通信了。那么,在React开发中,有哪些场景的组件通信?又如何去实现组件的通信呢?

总的来说,有两种场景:

  • 父子组件通信

  • 兄弟组件通信

下面简单介绍一下日常开发中比较常用的三个通信方式:


props传递

首先我们先来看看最常用的一个手段,通过props属性。以父子组件为例,父组件只需要将数据以props的方式传递给子组件,子组件可以直接通过this.props来获取,比如:

// 父组件 Parent
export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      message: '传给子组件的消息'
    }
  }
  // 消息回调
  onMessage(messageFromChildren) {
    console.log(messageFromChildren);
  }
  render() {
    const { message } = this.state;
    return (
      <div>
        <Children message={ message } onMessage={ this.onMessage.bind(this) } />
      </div>
    );
  }
}
// 子组件 Children
export default class Children extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    this.props.onMessage('来自子组件的消息');
  }
  render() {
    const { message } = this.props;
    return (
      <div>
        <p>{ message }</p>
        <button onClick={ this.handleClick.bind(this) }>click</button>
      </div>
    );
  }
}

当然,如果Children子组件需要传递数据给到父组件,可以使用回调方式,父组件将方法的引用通过props传递给到子组件,如上代码中的handleClick里调用了onMessage。当父组件的state更新时,Children组件会重新渲染,使用最新的message数据。


如果是兄弟组件的通信,就稍微有一点点不同,兄弟组件不能直接相互通信,需要通过父组件来中转一下,进行状态提升。兄弟组件将需要共享的数据提升至共同的直接父组件中,然后就跟普通的父子组件通信一样了。比如:

// 父组件 Parent
export default class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      messageFromA: '',
      messageFromB: ''
    }
  }
  onMessage(messageFromChildren, from) {
    console.log(messageFromChildren);
    this.setState({
      [from == 'A' ? 'messageFromA' : 'messageFromB']: messageFromChildren
    });
  }
  render() {
    const { messageFromA,  messageFromB} = this.state;
    return (
      <div>
        <ChildrenA message={ messageFromB } onMessage={ this.onMessage.bind(this) } />
        <ChildrenB message={ messageFromA } onMessage={ this.onMessage.bind(this) } />
      </div>
    );
  }
}
// 子组件ChildrenA
export default class ChildrenA extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    this.props.onMessage('来自A子组件的消息', 'A');
  }
  render() {
    const { message } = this.props;
    return (
      <div className="p-a b-a">
        <p>{ message }</p>
        <button onClick={this.handleClick.bind(this)}>clickA</button>
      </div>
    );
  }
}
// 子组件 ChildrenB
export default class ChildrenB extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    this.props.onMessage('来自B子组件的消息', 'B');
  }
  render() {
    const { message } = this.props;
    return (
      <div className="p-a b-a">
        <p>{ message }</p>
        <button onClick={this.handleClick.bind(this)}>clickB</button>
      </div>
    );
  }
}

f5df7787c12acd6cedf6916e3af94d21.png

当点击clickA的时候,子组件B接收到了子组件A的消息,反之亦然。


通过props的组件通信比较简单,但也有其自身的缺陷,当组件层级大于3层时,这种方式就不适合了,首先是深层级的传递对到维护来说简直就是噩梦,需要一层一层的看才能知道数据的来源及流向。其次的话,假如不止A、B子组件,还有C子组件的,A、B组件引发的父组件state更新会触发C子组件的更新,但事实上,C子组件并没有接收任何数据,容易造成资源浪费。

// Parent组件
  render() {
    const { messageFromA,  messageFromB} = this.state;
    return (
      <div>
        <ChildrenA message={ messageFromB } onMessage={ this.onMessage.bind(this) } />
        <ChildrenB message={ messageFromA } onMessage={ this.onMessage.bind(this) } />
        <ChildrenC />
      </div>
    );
  }
// 子组件 ChildrenC
export default class ChildrenC extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidUpdate() {
    console.log('ChildrenC updated');
  }
  render() {
    return (
      <div>ChildrenC</div>
    );
  }
}

0aa3d179d682d378194759bad334893d.png

DingTalk20171023221535.png


Publish/Subscribe模式

发布订阅模式又叫观察者模式,在React组件间通信时,完全可以使用这种模式。这种通信方式与单向数据流不同,不用将数据一层层的往下传递,避免了深层数据传递的带来的不便,使用起来也相当的简单。举个例子:

下面是一个非常简略的发布订阅代码,生产使用时建议使用成熟的第三方库:比如:EventEmitter

const EventEmitter = {
  events: {},
  addListener(type, callback) {
    !this.events[type] && (this.events[type] = []);
    this.events[type].push(callback);
  },
  removeListener(type) {
    this.events[type] && delete this.events;
  },
  trigger(type, data) {
    if(!this.events[type]) {
      return false;
    }
    this.events[type].forEach(func => {
      func(data);
    });
  }
};

// 组件代码如下:
// Parent 父组件
import event from './event';
export default class Parent extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    event.addListener('B', (message) => {
      console.log(`Parent父组件订阅ChildrenB触发的事件------${message}`);
    });
  }
  componentWillUnmount() {
    event.removeListener('B');
  }
  render() {
    return (
      <div>
        <ChildrenA />
        <ChildrenB />
      </div>
    );
  }
}
// ChildrenA组件
import event from './event';
export default class ChildrenA extends React.Component {
  constructor(props) {
    super(props);
  }
  componentDidMount() {
    event.addListener('B', (message) => {
      console.log(`ChildrenA组订阅ChildrenB触发的事件————${message}`)
    })
  }
  componentWillUnmount() {
    event.removeListener('B');
  }
  render() {
    return (
      <div>ChildrenA</div>
    );
  }
}
// ChildrenB组件
import event from './event';
export default class ChildrenB extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    event.trigger('B', '来自ChildrenB组件触发的消息')
  }
  render() {
    return (
      <div>
        ChildrenB
        <button onClick={this.handleClick.bind(this)}>clickB</button>
      </div>
    );
  }
}

当点击ChildrenB组件的clickB按钮时,会发布一个消息,由于父组件和ChildrenA子组件都订阅了消息,所以都会触发回调函数获取到ChildrenB子组件发过来的数据,从而达到父子、兄弟组件间的通信。

64c98ed3ed1631fc23d1a823da1f353f.png


发布订阅模式虽然使用起来非常方便,但是也是有缺点的,最主要的是在复杂的应用中,大量使用此模式会导致数据流向非常混乱,没有规则,相互交叉。如果是不熟悉的人接手项目,甚至连消息订阅的地方都不好找,只能通过搜索查找,很影响开发效率。其次,容易忘记在componentWillUnmount钩子中取消订阅,当组件再次渲染时,会重复订阅。

DingTalk20171023221703.png


Redux

另外一种就是耳熟能详的Redux了,在开发中一般是结合react-redux一起使用。相对来说,redux上手难度比较大,API虽然不多,但是要清晰理解每个API的原理及作用还是要花点功夫去深入学习的。目前,我自身对redux的理解程度还很浅薄,仅仅还处于知道怎么用这个阶段,如有写的不对的地方,不吝赐教。

redux主要由Action、Reducer及Store三部分组成,以下是三者的简单介绍:


Action

Action是把数据传到store的有效载荷,是store数据的唯一来源,它是纯粹的javascript对象,其中type属性是必须的,type的含义是表示此Action的作用,用来干什么事情。


Reducer

Reducer是用来响应Action描述的操作,比如一个Action是{type: 'ADD_MESSAGE', data: 'this is message'},Reducer可以根据type值来针对性的操作state,然后返回一个全新的state,不直接修改旧的state。


Store

某种意义上来说,Store算是粘合剂,用来关联Action和Reducer。通过其暴露的dispatch API可以调度一个Action来触发Reducer更新state。一个应用只有一个顶层的Store,管理所有的state,如果需要拆分数据逻辑,可以将Reducer拆分为多个更细粒度的Reducer,然后通过combineReducers合并成一个,并给到createStore来创建顶层Store。

以上的概念是个人的理解及粗糙的介绍,更多的可以看Redux文档,接下来看看如何用Redux来进行组件通信,简单例子:

// page组件
import { connect } from 'react-redux';
import { addMessage } from './actions';
class Page extends React.Component {
  constructor(props) {
    super(props);
  }
  handleClick() {
    const { dispatch, messageList} = this.props;
    dispatch(addMessage(`message: ${messageList.length + 1}`));
  }
  render() {
    const { messageList } = this.props;
    return (
      <div>
        <button onClick={this.handleClick.bind(this)}>添加message</button>
        <ul>
          {messageList.map((item, index) => {
            return <li key={index}>{item}</li>;
          })}
        </ul>
      </div>
    );
  }
}
export default connect((state)=> {
  return {
    messageList: state.messageList
  }
})(Page);
// reducer.js
import * as actions from './actions';
// 初始state
const initialState = {
  messageList: []
};
export default function index(state = initialState, action = {}) {
  switch (action.type) {
    case actions.ADD_MESSAGE:
      return Object.assign({}, state, {
        messageList: [...state.messageList, action.message]
      });
    default:
      return state;
  }
}
// actions.js
export const ADD_MESSAGE = 'ADD_MESSAGE';
export function addMessage(message) {
  return {
    type: ADD_MESSAGE,
    message
  };
}

62e8345f79b6b0da6aab820762b4c2c1.png

page组件中的button和ul是兄弟组件(这里button和ul没有进行二次包装),当点击button后,dispatch调度了一个增加message的action(addMessage),并触发了reducer中匹配的case(actions.ADD_MESSAGE),返回了最新的state。

page组件中用到了react-redux的connect,作用是把组件和store连接起来,它接收四个参数,分别是 mapStateToProps , mapDispatchToProps , mergeProps 和 options。相对来说,前两个用的最多,分别把state数据和dispatch通过props的形式传递给组件使用。当state数据变化是,都会重新调用mapStateToProps,得到最新的state,更新给到组件。


redux的单一数据源(顶层store)和可预测性加强了应用的可控性,我们能够清楚的知道数据是怎么变化或修改的,比如使用Redux-Dev-Tools可视化数据跟踪。

不过,使用redux会发现我们经常会写重复性的代码,代码量增加了不少,打包之后的文件可能会比较大,另外就是connect的滥用了,比较好的实践是在业务组件的顶层connect,然后通过props传递给子组件,保持单向数据流。

DingTalk20171023221808.png


除此之外,还有其他的通信方式,比如使用react的context、 transdux、mobx等,无论哪种通信方式都有其使用的场景,也有其优缺点。找到适合自身开发亦或符合业务需求的才是最好的,但有一点需要谨记,不要自己用起来一时爽,却给后人留了坑。当选择某种方式时,还是要从各方面去考量,比如开发效率、后期维护、扩展性等等。