在我们开发使用 React 的过程中,经常需要组件之间相互传递信息,故复习一下我们常见的几个组件间的通信场景:
一、父组件向子组件通信
这是我们平时最常见也是用得最多的一种通信方式,父组件向子组件传递 props, 子组件通过获取 props 中的内容得到父组件传递的信息。
二、子组件向父组件通信
2.1 使用回调函数属性
这个方法也是react最常见的一种方式,父组件向子组件传递一个回调函数属性, 子组件通过调用父组件传递的回调函数, 从而将数据传给父组件,实现子组件向父组件通信,它的使用其实也是通过props进行传递,所以优缺点与上面描述的props一致,且如果层级较深,需要逐层传递回调函数,可能导致代码冗余和维护困难:
import React, { useState } from "react";
import { Input } from "antd";
const Childref = (props, ref) => {
const [value, setValue] = useState("");
return (
<div>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
props.updateValue(e.target.value);
}}
placeholder="我是子组件的输入框"
/>
</div>
);
};
class App extends React.Component{
updateValue = (v) => {
console.log(v, '我拿到了子组件输入框的值');
}
render(){
return(
<Childref updateValue={this.updateValue} />
);
}
}
2.2 使用ref
2.2.1 父组件、子组件均是class组件
在父组件中,用 React.createRef() 创建 ref 对象,将其赋值给组件的实例属性,将 ref 绑定到组件,在父组件可以用ref.current访问子组件上所有方法和属性:
class Child extends React.Component<any, IProps> {
render() {
return (
<div>
<Button
onClick={() => {
this.props?.callback("我收到了,你可以不要再传了");
}}
>
点我
</Button>
父组件输入框的文案:{this.props.text}
</div>
);
}
}
class App extends React.Component {
ref = React.createRef(null);
render() {
return (
<div>
<Button
onClick={() => {
console.log(ref.current);
}}
>
点我
</Button>
<Child ref={this.ref} />
</div>
);
}
}
2.2.2 父组件是class组件,子组件是function组件
在父组件中,用 React.createRef() 创建 ref 对象,将其赋值给组件的实例属性,将ref绑定到组件上,但是我们可以看见,如果单单只是这样使用,会发现ref.current获取到的是null,所以这个时候我们要对function子组件进行forwardRef进行包裹,才能确保父组件能够访问到子组件的方法和属性:
import React, { useState, forwardRef } from "react";
import { Input } from "antd";
const Childref = forwardRef((props, ref) => {
const [value, setValue] = useState("");
return (
<div ref={ref}>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="我是子组件的输入框"
/>
</div>
);
});
class App extends React.Component{
ref = React.createRef(null);
render() {
return (
<div>
<Button
onClick={() => {
console.log(ref.current);
// 提问:这时候打印出来的ref.current就是子组件的dom元素,如果想要获取子组件内部的属性和方法呢??
}}
>
点我
</Button>
<Childref ref={ref} />
</div>
);
}
}
回答
这时候就要用上useImperativeHandle了。【react版本16.8以后】
useImperativeHandle 接受三个参数:
第一个参数 ref : 接受 forWardRef 传递过来的 ref 。
第二个参数 createHandle :处理函数,返回值作为暴露给父组件的 ref 对象。
第三个参数 deps :为可选参数,为函数的依赖变量。凡是函数中使用到的数据变量都需要放入deps中,如果处理函数没有任何依赖变量,可以忽略第3个参数。依赖项改变的情况第一个参数会重新调用,如果依赖项不传则每次render都会调用。如果依赖项传一个空数组,则第一个方法只会在初始化的时候调用一次,里面的值不是最新的。
const Childref = forwardRef((props, ref) => {
const [value, setValue] = useState("");
useImperativeHandle(ref, () => ({
value,
setValue
}));
return (
<div>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="我是子组件的输入框"
/>
</div>
);
});
class App extends React.Component{
ref = React.createRef(null);
render() {
return (
<div>
<Button
onClick={() => {
console.log(ref.current);
// 提问:如果想要获取子组件上的全部属性和方法呢?需要枚举式的在useImperativeHandle上挂载吗?
>
点我
</Button>
<Childref ref={ref} />
</div>
);
}
}
2.2.3 父组件是function组件,子组件是class组件
在父组件中,用 useRef() 创建 ref 对象,将其赋值给组件的实例属性,将 ref 绑定到组件,在父组件可以用ref.current访问子组件上所有方法和属性:
class Child extends React.Component<any, IProps> {
render() {
return (
<div>
<Button
onClick={() => {
this.props?.callback("我收到了,你可以不要再传了");
}}
>
点我
</Button>
父组件输入框的文案:{this.props.text}
</div>
);
}
}
function App(){
const ref = useRef(null);
return (
<div>
<Button
onClick={() => {
console.log(ref.current);
}}
>
点我
</Button>
<Child ref={ref} />
</div>
);
}
2.2.4 父组件、子组件均是function组件
import React, { useState, forwardRef } from "react";
import { Input } from "antd";
const Childref = forwardRef((props, ref) => {
const [value, setValue] = useState("");
return (
<div ref={ref}>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
placeholder="我是子组件的输入框"
/>
</div>
);
});
function App() {
const ref = useRef(null);
return (
<div>
<Button
onClick={() => {
console.log(ref.current);
// 提问:这时候打印出来的ref.current就是子组件的dom元素,如果想要获取子组件内部的属性和方法呢??
}}
>
点我
</Button>
<Childref ref={ref} />
</div>
);
}
总结
以上四种方式看出来两点,一个是父组件是class还是function组件只影响了在外面创建实例的方式,但是对于我们获取到的ref具体对象产生影响的是子组件的类型,如果是类组件,是无需做其他处理的 ,可以直接绑,但如果是函数式组件,本身是没有实例的,所以需要用forwardRef + useImperativeHandle进行辅助。
总结起来,useImperativeHandle的原理是通过将子组件内部定义的方法和属性绑定到ref对象上,从而使父组件可以通过ref.current访问和调用子组件的方法和属性。这样可以实现子组件向父组件暴露指定接口的目的,从而实现组件之间的通信和数据传递。
三、 跨级组件间通信
跨级组件是指父组件的子组件的子组件,或者更深层的嵌套关系,跨级组件之间想要通信,有两种常见方式:
3.1 中间组件层层传递
使用props将信息逐层传递
3.2 使用 Context 对象
如果要 Context 发挥作用,需要用到两种组件,一个是 Context 生产者(Provider),通常是一个父节点,另外是一个 Context 的消费者(Consumer),通常是一个或者多个子节点。所以 Context 的使用基于生产者消费者模式。使用步骤如下:
1、React.createContext创建了一个Context对象,假如某个组件订阅了这个对象,当react去渲染这个组件时,会从离这个组件最近的一个Provider组件中读取当前的context值
2、Context.Provider: 每一个Context对象都有一个Provider属性,这个属性是一个react组件。
在Provider组件以内的所有组件都可以通过它订阅context值的变动。具体来说,Provider组件有一个叫value的prop传递给所有内部组件,每当value的值发生变化时,Provider内部的组件都会根据新value值重新渲染
3、那内部的组件该怎么使用这个context对象里的东西呢?
a、对于react16.x之前的版本,内部组件为class声明的有状态组件:我们可以把Context对象赋值给这个类的属性contextType,如下面所示的GrandChildContext组件
b、对于react16.x之后的版本,我们可以使用Context.Consumer包裹子组件,这也是Context对象直接提供给我们的组件,这个组件接受一个函数作为自己的child,这个函数的入参就是context的value,并返回一个react组件。可以将上面的GrandChildContext改写为b:
c、对于react16.8之后的hooks的写法:我们可以使用useContext(context)去订阅provider的value值。可以将上面的GrandChildContext改写为c:
const ThemeContext = React.createContext("#fff");
export default class App extends React.Component {
state = {
value: "#000"
};
render() {
return (
<ThemeContext.Provider value={this.state.value}>
<Button
onClick={() => {
this.setState({
value: this.state.value === "green" ? "#000" : "green"
});
}}
>
变色
</Button>
<ChildContext />
</ThemeContext.Provider>
);
}
}
function ChildContext() {
return (
<div>
<GrandChildContext />
</div>
);
}
// a种情况
class GrandChildContext extends React.Component {
static contextType = ThemeContext; // 16.3以及以上版本的区别
render() {
return <Button style={{ backgroundColor: this.context }} />;
}
}
// b种情况
// function GrandChildContext() {
// return (
// <ThemeContext.Consumer>
// {(value) => <Button style={{ backgroundColor: value }} />}
// </ThemeContext.Consumer>
// );
// }
// c种情况
// function GrandChildContext() {
// const theme = useContext(ThemeContext);
// return (
// <Button style={{ backgroundColor: theme }} />
// );
// }
tips: context对于解决react组件层级很深的props传递很有效,但也不应该被滥用。只有像theme、language等这种全局属性(很多组件都有可能依赖它们)时,才考虑用context。如果只是单纯为了解决层级很深的props传递,需要考虑其他方式。
局限性:
- 在组件树中,如果中间某一个组件 ShouldComponentUpdate returning false 了,会阻碍 context 的正常传值,导致子组件无法获取更新。
- 组件本身 extends React.PureComponent 也会阻碍 context 的更新。
四、非嵌套组件间通信
非嵌套组件,就是通信组件间没有任何包含关系,包括兄弟组件以及不在同一个父级中的非兄弟组件;对于非嵌套组件,可以利用这几种方式通信:
4.1 利用二者共同父组件进行通信
这种情况相当于将父组件当作数据管理中间商了,两个兄弟组件无法直接沟通,那就让父组件暂存并转达一下消息,这种情况在我们项目中也经常用到,不举例了。
4.2 使用发布订阅的方式
发布订阅PubSubJS GitHub地址**
**如果采用组件间共同的父级来进行中转,会增加子组件和父组件之间的耦合度,如果组件层次较深的话,找到二者公共的父组件不是一件容易的事,使用发布订阅的方式来实现非嵌套组件间的通信,但他的缺点是事件订阅和发布的管理可能较为复杂,不易维护。
import React, { Component } from "react";
import PubSub from "pubsub-js";
export default class List extends Component {
state = {
//初始化状态
users: [], //users初始值为数组
isFirst: true, //是否为第一次打开页面
isLoading: false, //标识是否处于加载中
err: "" //存储请求相关的错误信息
};
token: any = null;
componentDidMount() {
this.token = PubSub.subscribe("refresh", (_, stateObj) => {
this.setState(stateObj);
});
}
componentWillUnmount() {
PubSub.unsubscribe(this.token);
}
render() {
const { users, isFirst, isLoading, err } = this.state;
return (
<div className="row">
{isFirst ? (
<h2>欢迎使用,输入关键字,随后点击搜索</h2>
) : isLoading ? (
<h2>Loading......</h2>
) : err ? (
<h2 style={{ color: "red" }}>{err}</h2>
) : (
users.map((userObj) => {
return (
<div key={userObj.id} className="card">
<p className="card-text">{userObj.name}</p>
</div>
);
})
)}
</div>
);
}
}
import React, { Component } from "react";
import PubSub from "pubsub-js";
import axios from "axios";
export default class Search extends Component {
keyWordElement: any = {
value: ""
};
search = () => {
//获取用户的输入(连续解构赋值+重命名)
const {
keyWordElement: { value: keyWord }
} = this;
//发送请求前通知List更新状态
PubSub.publish("refresh", { isFirst: false, isLoading: true });
//发送网络请求
setTimeout(() => {
PubSub.publish("refresh", {
isLoading: false,
users: [
{
id: 1,
name: "这是一条假数据"
}
]
});
}, 2000);
};
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">搜索github用户</h3>
<div>
<input
ref={(c) => (this.keyWordElement = c)}
type="text"
placeholder="输入关键词点击搜索"
/>
<button onClick={this.search}>搜索</button>
</div>
</section>
);
}
}
4.3 使用redux通信
这种方式是我们目前比较大面积使用的,因为很多数据不会频繁更新,但需要在很多并不一定有关系的组件中都需要用到,所以我们将数据都存在redux中进行统一管理,这样所有的组件都可以通过connect绑定我们的状态池获取自己当前所需要的数据进行操作。它的缺点我们也都有感触,就是代码复杂,需要引入额外的依赖和配置,对于简单场景会增加开发成本【此处不举例啦~】
4.4 Global Variables
哈哈哈,这种肯定轻易不要使用啦,我们项目中只在配置文件中用到了global variables,但是并不是用于组件之间通信,只适用于方便管理和运维,严格意义上来说可以算是开发与运维、测试之间的通信吧~
五、对比
| 方式 | 使用场景 | 优点 | 缺点 |
|---|---|---|---|
| props | 单向数据流、父子组件之间的简单通信 | 简单易用,适用于简单组件间的通信。 | 只适用于父子组件之间的通信,组件层级比较较深时传递起来就比较麻烦。 |
| ref | 需要访问子组件的实例或DOM元素、触发子组件的方法的时候 | 灵活性高,可以直接访问子组件的实例或者dom元素。 | 破坏了React的数据流一致性,不推荐过度使用。 |
| callback | 子组件向父组件传递数据或组件之间的双向通信 | 简单明了,适用于父子组件之间的通信或组件之间的双向通信 | 如果层级较深,需要逐层传递回调函数,可能导致代码冗余和维护困难 |
| context | 可用于像Theme、Language等设置类全局属性,或跨组件层级的数据共享,避免逐层传递数据 | 可以直接访问子组件的实例或DOM元素,灵活性高 | 上下文的更新会引发整个消费者子树的重新渲染,性能开销较大。 |
| 发布订阅 | 非父子组件之间的通信、跨组件层级的通信 | 灵活、适用于非父子组件之间的通信。 | 事件订阅和发布的管理可能较为复杂,不易维护。 |
| redux | 多个组件共享状态、组件之间的复杂通信 | 全局状态管理,方便组件之间的通信和状态共享 | 代码复杂,需要引入额外的依赖和配置,对于简单场景会增加开发成本。 |
| 全局变量Global Variables | 适用于极少数地方修改、而多处使用且不易重复的变量名,但不推荐大面积使用 | 全局可见,任何一个函数或线程都可以读写全局变量-同步操作简单;内存地址固定,读写效率比较高。 | 过多的全局变量会占用较多的内存单元;破坏了函数的封装性能;使函数的代码可读性降低. |
总结
本文记录了不同嵌套关系的组件间常用的几种通信方式:
- 父子嵌套关系:利用 props 对象实现父组件向子组件通信;
- 父子嵌套关系:利用 callback(回调函数) 或ref实现子组件向父组件通信;
- 多层(父子)嵌套关系(跨级通信):利用 Context 对象, 以生产者和消费者的方式实现通信;
- 非嵌套关系:利用 PubSubJS (发布订阅) 的方式实现通信;
- 示例:以上提到的一些通信方式****