React 组件之间是怎么通信的呢?

前言

在学习React的过程中,需要用到很多组件之间传递数据的场景,根据需求复杂程度,我们有以下几种组件通信模式:

  • 父组件向子组件通信
  • 子组件向父组件通信
  • 跨级组件之间通信
  • 非嵌套组件间通信

根据组件关系的不同,我们用不同的处理方式解决,以下是具体处理方式。

父组件向子组件通信

这是最简单的通信模式,如图:

image

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class List extends Component {
render() {
return (
<div>
<h1>this is List</h1>
<h2>The Index component data is: {this.props.data}</h2>
</div>
);
}
}

class Index extends Component {
render() {
return (
<div>
<h1>this is Index</h1>
<List data={"Index Content"} />
</div>
);
}
}

我们只要将传的数据在父组件prop直接向下传递即可。

子组件向父组件通信

当子组件需要向父组件传递信息的时候,我们可以使用父组件props给子组件的回调函数,利用在函数中的参数传递信息,比如需要向父组件传递一个颜色值,改变父组件文字的颜色。

image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class List extends Component {
render() {
//我们可以理解为color参数是子组件向父组件传递的数据

return (
<div>
<h1>this is List</h1>
<button onClick={()=>this.props.changeColor('red')} >changeIndexColor</button>
</div>
);
}
}

class Index extends Component {
constructor(){
super();
this.state={
color:'green'
}
}
changeColor(color){
this.setState({
color:color
})
}
render() {
return (
<div>
<h1 style={{color:this.state.color}} >this is Index</h1>
<List changeColor={this.changeColor.bind(this)} />
</div>
);
}
}

以上两种组件通信方式较为简单,但是当组件之间关系比较复杂,这个时候数据传递起来遍不是那么得心应手,这个时候我们可以“浅试”一下两种方式,为什么说是“浅试”,因为官方并不推荐你这么做。

跨级组件之间的通信

概念

跨级组件通信,就是父组件与“子子”组件的通信,向更深层的子组件通信,如图:

image

对于这种组件间通信我们可以考虑一下两种方式:

  • 父组件向子组件层层传递props
  • 使用context对象

第一种方式中,如果父子组件层次比较浅可以考虑使用props传递,但是当层次非常深时,那么就显得很臃肿了。

contxet相当于一个全局的变量,是一个父组件给其子组件共享的大容器,里面可以放置要通信的各种变量与函数,注意,只能子组件访问父组件的context,这样,当嵌套再深,也不是什么问题,不需要层层传递,子组件直接从最顶层的父组件获取即可。

方法

使用context需要满足以下两个条件:

  • 父组件需要申明自己支持context,定义一个childContextTypes表明context中属性的PropTypes,并且还需要提供一个函数getChildContext,用来返回相应的context对象
  • 子组件需要定义一个contextTypes静态变量表示自己所需要父组件哪些context对象

看起来简单,但是操作起来却没那么方便,因为React并不想你使用过多的context,context本身就是一个非常危险的方式,就像所有人都建议你不要使用全局变量一样。

需求:我们假设做这样一个demo,子子组件获取到顶层父组件的state属性设置自己文字颜色,和获取父组件的函数,改变父组件的文字大小。

父子组件Index,需要满足要求:父组件需要申明自己支持context,定义一个childContextTypes表明context中属性的PropTypes,并且还需要提供一个函数getChildContext,用来返回相应的context对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Index extends Component {
constructor() {
super();
this.state = {
listItemcolor: "blue",
indexSizesize: "30px"
};
}
//父组件声明自己提供给自组件的context对象
static childContextTypes = {
listItemcolor: PropTypes.string,
changeIndexSize: PropTypes.func
};

// 父组件返回给子组件相应的context对象
getChildContext() {
return {
listItemcolor: this.state.listItemcolor,
changeIndexSize: this.changeIndexSize.bind(this)
};
}

//提供给子子组件的函数,子子组件用来改变父组件的fontSize
changeIndexSize(size) {
this.setState({
indexSizesize: size
});
}

render() {
const indexSize = {fontSize:this.state.indexSizesize}
return (
<div>
<h1 style={indexSize} > this is Index </h1>
<List />
</div>
);
}
}

子组件List:

1
2
3
4
5
6
7
8
9
10
class List extends Component {
render() {
return (
<div>
<h1>this is List</h1>
<ListItem />
</div>
);
}
}

子子组件ListItem,满足要求:子组件需要定义一个contextTypes静态变量表示自己所需要父组件哪些context对象,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class ListItem extends Component {
//子组件声明自己需要使用的context
static contextTypes = {
listItemcolor: PropTypes.string,
changeIndexSize: PropTypes.func
};

render() {
//引入父组件context提供的state属性
const style = { color: this.context.listItemcolor };
//引入父组件context提供的函数
const changeIndexSize = size => {
return () => {
this.context.changeIndexSize(size);
};
};
console.log(changeIndexSize);
return (
<div>
<h2 style={style}>this is listItem</h2>
<button onClick={changeIndexSize("60px")}>change Index's Size</button>
</div>
);
}
}

总结

context增强了组件之间的耦合性,并且context就像全局变量一样,可以随意被修改,即子组件都能修改context的内容,导致程序出现难查的BUG,因此非常不建议大量使用。但是contrxt的在React具有很重要的地位,因为基本上所有的第三方状态管理库(Redux,Flux)都充分利用了context的机制,使得我们再大项目中完美的管理好复杂的状态,我们只要用好这些管理工具就好了。

非嵌套组件之间通信

概念

非嵌套组件,就是关系性很弱甚至没有包含关系的组件,就像如下图:

image

按照我们之前用过的三种方式,我们可以采用以下两种:

  • 利用二者的父组件的context对象进行通信
  • 利用自定义事件通信

我们权衡第一种方式,第一它采用组件的共同父级来进行中转,会增加子组件与父组件之间的耦合度,当两者共同的父组件出现修改时,指不定就会影响到context的传递。第二,如果两组件之间的层次非常非常深,找到两者的共同组件是一件不容易的事,emmmm,不是不可,只是不推荐这么做。

因此,我们采用一种全新的方式:自定义事件

方法

先下载自定义事件需要的包(create-react-app不会自带):

1
npm install events --save

在更目录下新建一个events.js文件,引入events包,并且向外提供事件对象,供组件之间通信时使用:

1
2
3
// events.js   
import { EventEmitter } from 'events';
export default new EventEmitter();

共同父组件Index:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// index.js  
import React, { Component } from "react";
import ReactDOM from "react-dom";
import List from "./List";
import Footer from "./Footer";

class Index extends Component {
render() {
return (
<div>
<List />
<Footer />
</div>
);
}
}

ReactDOM.render(<Index />, document.getElementById("root"));

List组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// List.js 
import React, { Component } from "react";
import emitter from "./events"; //引入events包

export default class List extends Component {
constructor(props) {
super(props);
this.state = {
color: "green"
};
}

//当组件挂载完毕,添加一个监听事件,可以理解为这个事件存储在全局环境当中,其他组件只要引入便可触发
componentDidMount() {
this.eventEmitter = emitter.addListener("changeColor", color => {
this.setState({
color
});
});
}

//当组件销毁时,监听函数自动销毁
componentWillUnmount() {
emitter.removeListener(this.eventEmitter);
}

render() {
const style = { color: this.state.color };
return (
<div>
<h1 style={style}>This is List</h1>
</div>
);
}
}

Footer组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Footer.js
import React, { Component } from "react";
import FooterBox from "./FooterBox";

export default class Footer extends Component {
render() {
return (
<div>
<h1>This is Footer</h1>
<FooterBox />
</div>
);
}
}

FooterBox组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//FooterBox.js
import React, { Component } from "react";
import emitter from "./events";

export default class FooterBox extends Component {
render() {
const changeColor = color => {
return () => {
emitter.emit("changeColor", color); //获取到监听函数
};
};
return (
<div>
<h1>This is FooterBox</h1>
<button onClick={changeColor("Blue")}>
FooterBox change List's textColor
</button>
</div>
);
}
}

效果图:

image

总结

了解发布/订阅模式的同学便很容易理解,通过向事件对象上添加监听器出发事件来实现组件间的通信,这种方式对比于context来说更加简单粗暴,但是滥用自定义events会积攒较多的“全局事件”,可能会导致不可预料的BUG发生。

最后总结

以上四种组件通信方式涵盖了我们大部分开发场景,第一二种是我们常用到的,官方文档也推荐我们这么用,但是第三四种是在比较复杂的开发场景下使用的,官方并不推崇使用第三四种方式实现,因为当业务复杂性越来越高时,会产生极大的耦合度,影响后期的更新维护,这个时候我们便需要引入状态管理工具,Flux、Redux、Mobx都是比较好的选择,关于状态管理工具后续也会有博文讲解。

nice to meet you~