React提高11 组件间通讯

React与Vue一样,都是通过组件化的思想来开发各个模块,这就必然面临着组件间通信的问题。项目中的组件间关系包括如下几种:

  1. 父子组件之间
  2. 爷孙组件之间(就是跨级的父子组件)
  3. 兄弟组件之间

各种组件间通信的方法都有不同的适用场景,下面分别学习一下。Demo中都使用了React Hooks API,顺道可以复习一下Hooks的基本用法。

通过传递props

React中,父组件向子组件传递数据是基础的组件间通信的方式,是通过props完成的。这种方式一般适用于父子组件之间通信。

父组件向子组件传递props进行通信,子组件向父组件通信则需要调用父组件通过props传递给子组件的函数(下面代码中的changeMsg),这个函数作用于是组父组件自身。子组件调用该函数,将子组件想要传递的信息作为参数,传递给父组件
``

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
const Child1 = props => {
return (
<div className={style['child-container']}>
<h4 onClick={() => props.changeMsg('By Child1')}>Child1 -- {props.msg}</h4>
<Grandson1 {...props} />
</div>
);
};

const Parent = () => {
const [msg, setMsg] = useState('start');

useEffect(() => {
setTimeout(() => {
setMsg('end');
}, 2000);
}, []);

return (
<div className={style['parent-container']}>
<h3>Parent -- {msg}</h3>
<Child1 msg={msg} changeMsg={newMsg => setMsg(newMsg)} />
</div>
);
};

export default function () {
return (
<div>
<h2>Demo14 -- 组件间通信</h2>
<Parent />
</div>
);
}

这种方式也适用于爷孙组件之间进行通信,但是需要通过子组件作为周转,在子组件中,使用...运算符将父组件的全部的props传递给更深层次的孙组件。孙子组件想爷爷组件传递消息也是通过...传递的函数进行的。

1
2
3
4
5
6
7
8
const Child1 = props => {
return (
<div className={style['child-container']}>
<h4 onClick={() => props.changeMsg('By Child1')}>Child1 -- {props.msg}</h4>
<Grandson1 {...props} />
</div>
);
};

至于兄弟组件之间,也可以使用这个方式进行通讯,但是必须借由相同的父组件,Child1将数据传递给Parent,再通过Parent将数据由props传递给Child2,组件比较多的时候不便于管理和维护。

观察者模式

可以引入第三方的观察者模式,来实现组件间的通信,这种方式适用于上面的所有情况。

观察者模式是一种比较常见的设计模式,观察者模式也叫做『发布-订阅』模式,发布者发布事件,订阅者监听事件并作出反应。

面试的时候也经常遇到手写建议的观察者模式的情况,下面是一种简易的实现:

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
export class EventEmitter {
constructor() {
this.events = {};
}

on(name, cb) {
if (!this.events[name]) {
this.events[name] = [];
}
this.events[name].push(cb);
}

emit(name, ...params) {
if (!this.events[name]) {
return;
}
this.events[name].forEach(cb => cb(...params));
}

off(name) {
if (!this.events[name]) {
return;
}
this.events[name] = [];
}
}

我们在Child2中订阅msg事件,并且在订阅的回调函数中修改其内部对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
const Child2 = () => {
const [msg, setMsg] = useState('Child2');

useEffect(() => {
event.on('msg', newMsg => setMsg(newMsg));
}, []);

return (
<div className={style['child-container']}>
<h4>Child2 -- {msg}</h4>
</div>
);
};

然后在Child1中发布msg事件,发布的时候就可以将数据从Child1传递到Child2中(实际上发布订阅模式就是将函数进行传递,在上面的例子中,我们是将setMsg函数作为参数传递到了订阅的回调函数中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Child1 = props => {
useEffect(() => {
setTimeout(() => {
event.emit('msg', 'Message from Child1');
}, 3000);
}, []);

return (
<div className={style['child-container']}>
<h4 onClick={() => props.changeMsg('By Child1')}>Child1 -- {props.msg}</h4>
<Grandson1 {...props} />
</div>
);
};

Redux

利用Redux框架可以管理复杂的组件间通讯的状况,它可以解决文章一开始提出的多种情况的组件间通讯。

之前学习过Redux,但是实际工作中没有用过,总是忘,但是这次回顾还是顺利的多,只不过没有实际经验,Redux的实际维护缺乏太多工程实践了。

下面的Demo使用了React-Redux,它提供了很多遍历的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
import React, { useState, useEffect } from 'react';
import { connect, Provider } from 'react-redux';
import store from '@/store/';

const mapStateToProps = (state) => ({ rootMsg: state.reducer14.msg });
const mapDispatchToProps = { changeMsg: where => ({ type: 'changeMsg', payload: { where }}) };

const Demo14 = ({ rootMsg, changeMsg }) => {
return (
<div>
<h2 onClick={() => changeMsg('Root')}>Demo14 -- 组件间通信 -- {rootMsg} </h2>
<Parent />
</div>
);
};

const Demo14Container = connect(mapStateToProps, mapDispatchToProps)(Demo14);

export default function () {
return (
<Provider store={store}>
<Demo14Container />
</Provider>
);
}

通过connect方法生成一个新的注入了Redux相关方法的组件,通过mapStateToPropsStore中的state映射为UI组件的prop,通过mapDispatchToPropsdispatch也映射为props的方法(有没有很熟悉的感觉,Vuex的mapState/mapMutations),通过<Provider>注入store

同样,也通过connect方法生成包装后的Grandson2Container组件:

1
2
3
4
5
6
7
8
9
const Grandson2 = ({ rootMsg, changeMsg }) => {
return (
<div className={style['grandson-container']}>
<h5 onClick={() => changeMsg('Grandson1')}>Grandson1 -- {rootMsg}</h5>
</div>
);
};

const Grandson2Container = connect(mapStateToProps, mapDispatchToProps)(Grandson2);

这样,根组件和Grandson2就通过Redux实现了通信,兄弟组件之间也同样可以实现。

Content API

之前学习过React的Content API,这个API也可以实现组件间通讯的功能。

这个API有过较大的变化,Demo中使用的都是新的API。要注意的是,官方文档也提到,使用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
40
41
42
43
// 初始化一个 Context
const Context = React.createContext();

const Grandson3 = () => {
return (
<Context.Consumer>
{context => (
<div className={style['grandson-container']}>
<h5 onClick={() => context.changeMsg('By Grandson3')}>Grandson3 -- {context.msgToGrand3}</h5>
</div>
)}
</Context.Consumer>
);
};

const Child3 = () => {
const [msg] = useState('Child3');

return (
<div className={style['child-container']}>
<h4>Child3 -- {msg}</h4>
<Grandson3 />
</div>
);
};

const Parent = () => {
const [msg, setMsg] = useState('start');

return (
<Context.Provider value={{
msgToGrand3: msg,
changeMsg: newMsg => setMsg(newMsg)
}}>
<div className={style['parent-container']}>
<h3>Parent -- {msg}</h3>
<Child3 />
</div>
</Context.Provider>
);
};

export default Parent;

这种方式首先通过React.createContext初始化一个xxxContext,然后xxxContext.Provider作为顶层组件,接受一个value作为prop,将数据以对象的形式传入(包括方法),xxxContext.Consumer作为目标组件,其内部通过一个函数返回需要接受数据的组件,这个函数的参数就是context,它就是从Provider中传过来的数据。

这种方式在有多个数据需要传递时,不太容易管理。

还有就是Context结合Hooks,可以实现Redux类似的数据共享的作用,可以参考这篇文章

参考