react组件通信的方式
# 简介
React 中组件间的通信有以下几种情况:
- 父组件向子组件通信,可以通过 props 方式传递数据;也可以通过 ref 方式传递数据;
- 子组件向父组件通信,通过回调函数方式传递数据;
- 跨级组件之间通信,
- 中间组件层层传递 props; 当组件层次在三层以内可以采用这种方式;
- 使用 context 对象
- 一个数据源实现跨组件通信,通过指定 contextType 的方式来实现;
- 多个数据源实现跨组件通信,使用 Context.Consumer 方式实现;
- 父组件向后代所有组件传递数据,如果组件层级过多,通过 props 的方式传递数据很繁琐,可以通过 Context.Provider 的方式;
- 非嵌套组件间通信
- 利用二者共同父组件的 context 对象进行通信
- 使用自定义事件的方式
# 父组件向子组件通信
父组件向子组件通信有两种方式,
- 一是父组件通过属性进行传递,子组件通过 props 接收;
- 二是父组件通过 ref 获取到子组件的实例或者元素,调用子组件的方法进行数据传递;
# props 方式传递数据
父组件中通过给子组件设置属性,将数据传递给子组件,子组件通过 props 来接收,当父组件更改自己状态的时候,子组件接收到的属性就会发生改变。
import React, { Component } from 'react'
import ReactDOM from 'react-dom';
class Child extends Component{
render(){
return <h1>接收到数据为:{this.props.num}</h1>
}
}
class Parent extends Component{
num=3;
render(){
return <Child num={this.num}></Child>
}
}
ReactDOM.render(
<Parent/>,
document.getElementById('root')
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ref 传递数据【推荐】
这种方式获取子组件的方法非常有用;
原理
React 提供的这个ref
属性,表示为对组件真正实例的引用,其实就是 ReactDOM.render()
返回的组件实例,ref
可以挂载到组件上也可以挂载到dom元素上。
- 挂到组件(
class
声明的组件)上的 ref 表示对组件实例的引用。不能在函数式组件上使用 ref 属性,因为它们没有实例。 - 挂载到dom元素上时表示具体的dom元素节点。
使用方法:
- 父组件使用
ref
属性对子组件做标记,获取到子组件的实例或者dom元素,可以调用子组件的方法,传递数据。 - 在 React 最新的版本中,要使用
ref
,需要通过React.createRef()
方法生成一个ref
。
import React, { Component ,createRef} from 'react'
import ReactDOM from 'react-dom';
class Child extends Component{
state={
name:"admin"
}
childClickHandle=(city)=>{
this.setState({
address:city
})
}
render(){
return (
<div>name:{this.state.name},address:{this.state.address}</div>
)
}
}
class Parent extends Component{
constructor(){
super();
this.childComp=createRef()//通过 createRef() 生成ref
}
clickHandle=()=>{
this.childComp.current.childClickHandle("samy"); //调用子组件的方法,并传递数据
}
render(){
return (
<div>
<button onClick={this.clickHandle}>按钮</button>
<Child ref={this.childComp}></Child> //给子组件设置ref属性
</div>
)
}
}
ReactDOM.render(
<Parent/>,
document.getElementById('root')
);
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
# 通过属性挂载方式
原理
当在子组件中调用onRef函数时,正在调用从父组件传递的函数。this.props.onRef(this)
这里的参数指向子组件本身,父组件接收该引用作为第一个参数:onRef = {ref =>(this.child = ref)}
然后它使用this.child
保存引用。之后,可以在父组件内访问整个子组件实例,并且可以调用子组件函数。
class Parent extends React.Component {
testRef=(ref)=>{
this.child = ref
console.log(ref) // -> 获取整个Child元素
}
handleClick=()=>{
alert(this.child.state.info) // -> 通过this.child可以拿到child所有状态和方法
}
render() {
return <div>
<Child onRef={this.testRef} />
<button onClick={this.handleClick}>父组件按钮</button>
</div>
}
}
class Child extends React.Component {
constructor(props) {
super(props)
this.state = {
info:'快点击子组件按钮哈哈哈'
}
}
componentDidMount() {
this.props.onRef(this)
console.log(this) // ->将child传递给this.props.onRef()方法
//const { Refs } = this.props;
//if (Refs) Refs(this);
// this.initData();
//this.getData();
}
handleChildClick=()=>{
this.setState({info:'通过父组件按钮获取到子组件信息啦啦啦'})
}
render(){
return <button onClick={this.handleChildClick}>子组件按钮</button>
}
}
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
# 子组件向父组件通信
子组件通过 回调函数
向父组件传递数据。父组件将自己的某个方法传递给子组件,子组件通过this.props
接收到父组件的方法后进行调用。
如果子组件内需要修改父组件传递过来的数据,需要通过调用父组件的方法,在父组件中对数据进行修改。
import React, { Component ,createRef} from 'react'
import ReactDOM from 'react-dom';
class Child extends Component{
state={
name:"samy",
age:18
}
childClickHandle=()=>{
this.props.showInfo({address:"sz"})
}
render(){
return (
<div>
//方式一:直接调用父组件的方法
<button onClick={this.props.showInfo.bind(this,this.state)}>按钮</button>
//方式二:先调用自身的方法,再调用父组件的方法
<button onClick={this.childClickHandle}>按钮</button>
</div>
)
}
}
class Parent extends Component{
clickHandle(data){
console.log(data); ////data为子组件中传递过来的数据{name: "samy", age: 18, sex: "man"}
}
render(){
return <Child showInfo={this.clickHandle.bind(this)}></Child>
}
}
ReactDOM.render(
<Parent/>,
document.getElementById('root')
);
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
# 跨组件通信 Context
所谓跨级组件通信,就是父组件向子组件的子组件通信,向更深层的子组件通信。跨级组件通信可以采用下面两种方式:
- 中间组件层层传递 props
- 使用 context 对象
# 简介
# Context 的使用场景
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据。请谨慎使用,因为这会使得组件的复用性变差。如果用组件组合可以解决的问题,就不要使用 Context 。
使用 context 的通用的场景包括管理当前的 locale,theme,或者一些缓存数据。
# Context 总结
React.createContext(defaultValue)
创建一个 Context 对象。Class.contextType
,挂载在 class 上的 contextType 属性会被赋值为一个 Context 对象。这能让你使用this.context
来消费最近的 Context 上的数据。你可以在任何生命周期中访问到它,包括 render 函数中。Context.Provider
,接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。Context.Consumer
,是函数作为子元素(function as a child)这种做法。这个函数参数接收当前的 context 值,返回一个 React 节点。传递给函数的value
等同于往上组件树离这个 context 最近的Provider 提供的 value 值
。如果没有对应的 Provider,value 参数等同于传递给createContext()
的defaultValue
。Context.displayName
,浏览器中调试用。
Consumer 一般情况下都是和 Provider 同时使用。
当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。
在 react 中没有类似 vue 中的事件总线来解决这个问题,只能借助它们共同的父级组件来实现,将非父子关系装换成多维度的父子关系。
react 中数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于多层级父子关系的组件传值是极其繁琐的。**react 提供了context
api 来实现在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。**React 16.3之后的context
api 较之前的好用。
# 父组件向后代组件通信
使用 Context.Provider ,具体使用方法如下:
- 使用
createContext()
创建一个context 对象(内容为需要传递的数据),参数为默认值; - 在父组件外层使用
<.Provider>
将当前 context 的值传递给内部所有的组件树。当使用了<.Provider>
后,不再读取上面的默认值,需要设置 value 属性来进行数据传递。 - 当组件需要读取数据时,指定
contextType
读取当前的 context 对象(即刚开始创建的 context); - 通过
this.context
,获取到当前 context 内的数据;
import React, { Component ,createContext} from 'react';
import ReactDOM from 'react-dom';
//使用createContext()创建一个context,参数为默认值
const cityContext = createContext("beijing");
cityContext.displayName = "cityContextName"; //devtools中调试用
class Parent extends Component{
render() {
return (
//使用Provider 将当前context的值传递给下面的组件树
<cityContext.Provider value="shenzhen">
<Child />
</cityContext.Provider>
)
}
}
//中间的组件不需要对数据进行传递
class Child extends Component{
render() {
return <Grandson />
}
}
class Grandson extends Component{
//指定contextType 读取当前的context
//react 会往上找到最近的 Provider,然后使用它的值
static contextType = cityContext;
render() {
return <div>{this.context}</div>
//最终页面上输出 shenzhen
}
}
ReactDOM.render(
<Parent/>,
document.getElementById('root')
);
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
上面的方法中,我们实现了跨组件的数据传递,这种方式的缺点是只能有一个共享的数据源,也就是在 Grandson 组件
中,指定 contextType 的值只能是一个
。那么如果我们有多个数据源都需要进行跨组件传递,应该怎么做呢?这里,我们可以使用 <.Consumer>
来实现对多个数据源进行共享。
# 多个数据源实现跨组件通信
使用 Context.Consumer ,实现多个数据源跨组件通信。具体使用方法如下:
- 使用 import
导入要读取的数据文件
。 - 使用<.Consumer> ,它里面的语法是
函数组件的语法
,函数接收的参数
为当前 createContext() 里的默认值。 - <.Consumer> 可以嵌套使用,也可以平级使用。
比如现在我们有两个数据文件 CityContext.js 和 WeatherContext.js (注意:共享的数据源文件需要导出 context 对象)。
CityContext.js 文件内容:
/* CityContext.js */
import {createContext} from 'react'
const CityContext = createContext({
id:1,
name:"beijing",
location:"north"
})
export default CityContext;
2
3
4
5
6
7
8
WeatherContext.js 文件内容:
/* WeatherContext.js */
import {createContext} from 'react'
const WeatherContext = createContext({
status:"sunshine"
})
export default WeatherContext;
2
3
4
5
6
上面这两个文件需要在多个组件内进行使用,可以使用下面的方式:
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
//导入需要使用的context对象
import CityContext from './CityContext';
import WeatherContext from './WeatherContext';
class Child extends Component{
render() {
return (
<CityContext.Consumer> //子元素是函数组件的语法
{
(prop)=>{ //参数是当前context对象里的数据
return (
<div>
name:{prop.name},location:{prop.location}
<WeatherContext.Consumer> //可以嵌套使用,也可以同级使用
{
({status})=>{
return <div>weather:{status}</div>
}
}
</WeatherContext.Consumer>
</div>
)
}
}
</CityContext.Consumer>
)
}
}
ReactDOM.render(
<Child />,
document.getElementById('root')
);
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
# 另外示范
使用 context 也很简单,需要满足两个条件:
- 上级组件要声明自己支持 context,并提供一个函数来返回相应的 context 对象
- 子组件要声明自己需要使用 context
下面以代码说明,我们新建 3 个文件:父组件 App.js,子组件 Sub.js,子组件的子组件 SubSub.js。 App.js:
import React, { Component } from 'react';
import PropTypes from "prop-types";
import Sub from "./Sub";
import "./App.css";
export default class App extends Component{
// 父组件声明自己支持 context
static childContextTypes = {
color:PropTypes.string,
callback:PropTypes.func,
}
// 父组件提供一个函数,用来返回相应的 context 对象
getChildContext(){
return{
color:"red",
callback:this.callback.bind(this)
}
}
callback(msg){
console.log(msg)
}
render(){
return(
<div>
<Sub></Sub>
</div>
);
}
}
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
Sub.js
import React from "react";
import SubSub from "./SubSub";
const Sub = (props) =>{
return(
<div>
<SubSub />
</div>
);
}
export default Sub;
2
3
4
5
6
7
8
9
10
SubSub.js
import React,{ Component } from "react";
import PropTypes from "prop-types";
export default class SubSub extends Component{
// 子组件声明自己需要使用 context
static contextTypes = {
color:PropTypes.string,
callback:PropTypes.func,
}
render(){
const style = { color:this.context.color }
const cb = (msg) => {
return () => {
this.context.callback(msg);
}
}
return(
<div style = { style }>
SUBSUB
<button onClick = { cb("我胡汉三又回来了!") }>点击我</button>
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
如果是父组件向子组件单向通信,可以使用变量,如果子组件想向父组件通信,同样可以由父组件提供一个回调函数,供子组件调用,回传参数。 在使用 context 时,有两点需要注意:
- 父组件需要声明自己支持 context,并提供 context 中属性的 PropTypes
- 子组件需要声明自己需要使用 context,并提供其需要使用的 context 属性的 PropTypes
- 父组件需提供一个 getChildContext 函数,以返回一个初始的 context 对象
如果组件中使用构造函数(constructor),还需要在构造函数中传入第二个参数 context,并在 super 调用父类构造函数是传入 context,否则会造成组件中无法使用 context。
constructor(props,context){
super(props,context);
}
2
3
# 改变 context 对象
我们不应该也不能直接改变 context 对象中的属性,要想改变 context 对象,只有让其和父组件的 state 或者 props 进行关联,在父组件的 state 或 props 变化时,会自动调用 getChildContext 方法,返回新的 context 对象,而后子组件进行相应的渲染。 修改 App.js,让 context 对象可变:
import React, { Component } from 'react';
import PropTypes from "prop-types";
import Sub from "./Sub";
import "./App.css";
export default class App extends Component{
constructor(props) {
super(props);
this.state = {
color:"red"
};
}
// 父组件声明自己支持 context
static childContextTypes = {
color:PropTypes.string,
callback:PropTypes.func,
}
// 父组件提供一个函数,用来返回相应的 context 对象
getChildContext(){
return{
color:this.state.color,
callback:this.callback.bind(this)
}
}
// 在此回调中修改父组件的 state
callback(color){
this.setState({
color,
})
}
render(){
return(
<div>
<Sub></Sub>
</div>
);
}
}
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
此时,在子组件的 cb 方法中,传入相应的颜色参数,就可以改变 context 对象了,进而影响到子组件:
return(
<div style = { style }>
SUBSUB
<button onClick = { cb("blue") }>点击我</button>
</div>
);
2
3
4
5
6
context 同样可以应在无状态组件上,只需将 context 作为第二个参数传入:
import React,{ Component } from "react";
import PropTypes from "prop-types";
const SubSub = (props,context) => {
const style = { color:context.color }
const cb = (msg) => {
return () => {
context.callback(msg);
}
}
return(
<div style = { style }>
SUBSUB
<button onClick = { cb("我胡汉三又回来了!") }>点击我</button>
</div>
);
}
SubSub.contextTypes = {
color:PropTypes.string,
callback:PropTypes.func,
}
export default SubSub;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 非嵌套组件间通信
非嵌套组件,就是没有任何包含关系的组件,包括兄弟组件以及不在同一个父级中的非兄弟组件。对于非嵌套组件,可以采用下面两种方式:
- 利用二者共同父组件的 context 对象进行通信
- 使用自定义事件的方式
# 自定义事件的方式
如果采用组件间共同的父级来进行中转,会增加子组件和父组件之间的耦合度,如果组件层次较深的话,找到二者公共的父组件不是一件容易的事,当然还是那句话,也不是不可以...
这里我们采用自定义事件的方式来实现非嵌套组件间的通信。
我们需要使用一个 events 包:npm install events --save
新建一个 ev.js,引入 events 包,并向外提供一个事件对象,供通信时使用:
import { EventEmitter } from "events";
export default new EventEmitter();
2
App.js
import React, { Component } from 'react';
import Foo from "./Foo";
import Boo from "./Boo";
import "./App.css";
export default class App extends Component{
render(){
return(
<div>
<Foo />
<Boo />
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Foo.js
import React,{ Component } from "react";
import emitter from "./ev"
export default class Foo extends Component{
constructor(props) {
super(props);
this.state = {
msg:null,
};
}
componentDidMount(){
this.eventEmitter = emitter.addListener("callMe",(msg)=>{
this.setState({
msg
})
});// 声明一个自定义事件; 在组件装载完成以后
}
componentWillUnmount(){
emitter.removeListener(this.eventEmitter);// 组件销毁前移除事件监听
}
render(){
return(
<div>
{ this.state.msg }
我是非嵌套 1 号
</div>
);
}
}
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
Boo.js
import React,{ Component } from "react";
import emitter from "./ev"
export default class Boo extends Component{
render(){
const cb = (msg) => {
return () => {
emitter.emit("callMe","Hello") // 触发自定义事件
}
}
return(
<div>
我是非嵌套 2 号
<button onClick = { cb("blue") }>点击我</button>
</div>
);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
自定义事件是典型的发布/订阅模式,通过向事件对象上添加监听器和触发事件来实现组件间通信。
# Redux
上面有提到使用 Context
做组件间的通讯会使得组件的复用性变差
,如果项目比较复杂、模块比较很多的情况,推荐使用 Redux
来做组件间的通信。
react-redux 提供两个核心的api:
- Provider: 提供store,根据单一store原则 ,一般只会出现在整个应用程序的最顶层。
- connect: 用于连接展示组件和 redux store。
connect 的语法格式为: connect(mapStateToProps?, mapDispatchToProps?, mergeProps?, options?)(component)
一般来说只会用到前面两个,它的作用是:
- 把
store.getState()
的状态转化为展示组件props
上的属性 - 把
actionCreators
转化为展示组件props
上的方法
特别强调: 官网上的第二个参数为mapDispatchToProps, 实际上就是actionCreators
只要上层中有Provider
组件并且提供了store
, 那么,子孙级别的任何组件,要想使用store
里的状态,都可以通过connect
方法进行连接。如果只是想连接actionCreators
,可以第一个参数传递为null
。