React 组件通信方式

React 组件根据层级关系可以划分成父组件(Parent)、子组件(Child)、子组件的兄弟组件(Brother)以及孙组件(Grandson),抛开三方数据流管理库(如 ReduxMobx 等)本文将从 React 自身探讨和总结以上四种组件之间的数据通信方式。

component-share.png

父子组件数据通信

1. Parent => Child

Parent 组件想把数据传给子组件,最直接简单的方式就是通过组件 props 把数据传递过去。

1
2
3
4
5
6
7
8
// 父组件
class Parent extends React.Component {
state = {data: 1}
render() {
return <Child data={this.state.data} />
}
}

第二种方法就是通过将数据或获取数据的方法挂载到 context 上面来供子组件使用,但是相比起直接传给子组件 props,这种方式在实现起来会麻烦很多,故不推荐。

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
// 父组件
class Parent extends React.Component {
state = {data: 1}
// 声明可以提供给子组件的 context 对象属性
static childContextTypes = {
data: PropTypes.number
}
// 返回 context 对象
getChildContext() {
return {
data: this.state.data
}
}
render() {
return <Child />
}
}
// 子组件
class Child extends React.Component {
// 声明自己需要父组件的 context 对象属性
static contextTypes = {
data: PropTypes.number
}
render() {
return <input value={this.context.data} />
}
}

上述方法的使用的是 React16.3 以下的版本 context api,而在 React16.3 之后的版本中,提供了新的 context api

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
// 创建一个 Context 对象
const Context = React.createContext()
// 父组件
class Parent extends React.Component {
state = {data: 1}
render() {
return (
<Context.Provider value={{data: this.state.data}}>
<Child />
</Context.Provider>
)
}
}
// 子组件
class Parent extends React.Component {
render() {
return (
<Context.Consumer>
{
context => <input value={context.data} />
}
</Context.Consumer>
)
}
}

2. Parent <= Child

Parent 组件需要获取 Child 组件内部数据有三种方法:

2.1 通过 props 传入回调函数

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
// 子组件
class Child extends React.Component {
constructor(props){
this.state = {data: 2}
props.getData(this.state.data)
}
// 输入变化
onChange = e => {
this.setState({data: e.target.value})
this.props.getData(e.target.value)
}
render() {
return <input onChange={this.onChange value={this.state.data} />
}
}
// 父组件
class Parent extends React.Component {
state = {data: 1}
// 获取子组件 data
getData = data => { this.state.data = data }
render() {
return <Child getData={this.getData} />
}
}

这种方式优点是简单,缺点是如果想保持 Parent 随时可以同步 Child 数据的话,需要在每个改变 Child 数据的地方执行 getData

2.2 通过 context 传入回调函数

props 传入回调函数类似,该方法将回调函数挂载到 context 对象上,子组件可以通过 context 中的回调,将自己的数据传给父组件,这种方式比起将回调挂载到 props 对象上实现起来更为繁琐,不推荐。

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
// Context 对象
const Context = React.createContext()
// 父组件
class Parent extends React.Component {
state = {data: 1}
// 获取子组件数据
getData = data => { this.state.data = data}
render() {
return (
<Context.Provider value={{getData: this.getData}}>
<Child />
</Context.Provider>
)
}
}
// 子组件
class Child extends React.Component {
state = {data: 2}
render() {
return (
<Context.Consumer>
{
context => <input
value={this.state.data}
onChange={e => {
this.setState({data: e.target,value})
context.getData(e.target.value)
}}
/>
}
</Context.Consumer>
)
}
}

2.3 通过 ref 调用 Child 内部方法

通常,每个 class 类型的 React.Component 都会有一个 ref 属性供它的外层组件使用,外层组件可以通过 ref 来拿到组件实例内部的方法和数据,利用这一点我们可以让父组件获取到子组件的数据。

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
// 父组件
class Parent extends React.Component {
state = {data: 1}
// 获取子组件数据
getData = data => { this.state.data = this.refs.child.getCurData() }
render() {
return <Child ref='child' />
}
}
// 子组件
class Child extends React.Component {
state = {data: 2}
// 输入值发生变化
onChange = e => {
this.setState({data: e.target.value})
}
// 返回当前最新的 data
getCurData = () => this.state.data
render() {
return <input onChange={this.onChange} value={this.state.data} />
}
}

这种方式灵活性比较高,子组件不需要在每次 data 发生改变的时候执行回调同步 dataParent,对于 Child 而言,只要保证 getCurData 返回的是最新的 data 即可,而 Parent 只要在需要 Child 数据的时候调用 getData 即可。

爷孙组件数据通信

从文章开始的图可以看出,爷孙组件之间的通信可以通过 props 依次下传的方式,也可以通过 context 对象直接进行数据通信,通过上面提及的父子组件的通信方式可以看出,采用 props 传递数据类似于瀑布流依次向下,而 context 对象则像一块共享空间,父组件和它的子孙组件都可以通过这个空间对数据予取予求,对于嵌套较深的子孙组件,如果采用 props 往下传的方式通信数据,会形成一条很长的 props 依赖链,这会造成以下问题:

  1. 很多不直接使用通信数据的组件,会被迫引入这个通信数据,然后接着往它的子组件传。

  2. 一旦父组件的数据格式发生变化,那么引入了该数据格式的子孙组件都需要进行相应修改。

而如果采用 context 对象让父组件和子孙组件跨层级共享数据可以解决 props 下传带来的两个问题,但同时,使用 context 对象也会带来副作用:

  1. 如果组件树比较庞大,父组件和子孙组件间的数据依赖关系可能不够明显,不利于后期代码的维护
  2. 对于 React16.3 版本以下的 context api,容易被 shouldComponentUpdate 函数截断,导致依赖 context 数据的组件无法在 context 数据变化时重新渲染

兄弟组件数据通信

通常兄弟组件之间如果需要数据通信,常见的做法是把需要通信的数据放置到父组件中进行管理,然后兄弟组件通过 props 或者 context 对象来读写数据。

任意组件数据通信

任意组件,或者说是不区分层次关系的组件,要想进行数据通信可以采用三方对象管理来管理要共享的数据,而需要这些数据的组件只需通过订阅这些数据的变化即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// EventEmitter 用来存储要通信的数据
const event = new EventEmitter()
// 组件 A
class A extends React.Component {
constructor(){
// 订阅数据变化
event.sub('dataUpdate', data => console.log(data))
}
}
// 组件 B
class B extends React.Component {
constructor(){
// 往 event 写数据
setTimeout(() => event.pub('dataUpdate', 1), 200)
}
}