根据组件之间的嵌套关系(即层级关系)可分为4种通信方式:父子、兄弟、跨级和无级。
## 一、父子通信
  在React中,数据是自顶向下单向流动的,而父组件通过props向子组件传递需要的信息是组件之间最常见的通信方式,如下代码所示,父组件Parent向子组件Child传递了一个name属性,其值为一段字符串“strick”。
~~~js
class Parent extends React.Component {
render() {
return <Child name="strick">子组件</Child>;
}
}
class Child extends React.Component {
render() {
return <input name={this.props.name} type="text" />;
}
}
~~~
  当需要子组件向父组件传递信息时,也能通过组件的props实现,只是要多传一个回调函数,如下所示。
~~~js
class Parent extends React.Component {
callback(value) {
console.log(value); //输出从子组件传递过来的值
}
render() {
return <Child callback={this.callback} />;
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
}
handle(e) {
this.props.callback(e.target.value); //调用父组件的回调函数
this.setState({ name: e.target.value }); //更新文本框中的值
}
render() {
return <input value={this.state.name} type="text" onChange={this.handle.bind(this)} />;
}
}
~~~
  父组件Parent会传给子组件Child一个callback()方法,子组件中的文本框注册了一个onChange事件,在事件处理程序handle()中将回调父组件的callback()方法,并把文本框的值传递过去,以此达到反向通信的效果。
## 二、兄弟通信
  当两个组件拥有共同的父组件时,就称它们为兄弟组件,注意,它们可以不在一个层级上,如图6所示,C与D或E都是兄弟关系。
:-: ![](https://img.kancloud.cn/94/97/94977caa098df65392b115251266d383_303x303.png =200x)
:-: 图6 组件树
  兄弟之间不能直接通信,需要借助状态提升的方式间接实现信息的传递,即把组件之间要共享的状态提升至最近的父组件中,由父组件来统一管理。而任意一个兄弟组件可通过从父组件传来的回调函数更新共享状态,新的共享状态再通过父组件的props回传给子组件,从而完成一次兄弟之间的通信。在下面的例子中,会有两个文本框(如图7所示),当向其中一个输入数字时,邻近的文本框会随之改变,要么加一,要么减一。
:-: ![](https://img.kancloud.cn/d4/7e/d47e85cbb2d26c7114f7c23790655947_714x86.gif =400x)
图7 两个文本框
~~~js
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { type: "p", digit: 0 };
this.plus = this.plus.bind(this);
this.minus = this.minus.bind(this);
}
plus(digit) {
this.setState({ type: "p", digit });
}
minus(digit) {
this.setState({ type: "m", digit });
}
render() {
let { type, digit } = this.state;
let pdigit = type == "p" ? digit : (digit+1);
let mdigit = type == "m" ? digit : (digit-1);
return (
<>
<Child type="p" digit={pdigit} onDigitChange={this.plus} />
<Child type="m" digit={mdigit} onDigitChange={this.minus} />
</>
);
}
}
class Child extends React.Component {
constructor(props) {
super(props);
this.handle = this.handle.bind(this);
}
handle(e) {
this.props.onDigitChange(+e.target.value);
}
render() {
return (
<input value={this.props.digit} type="text" onChange={this.handle} />
);
}
}
~~~
  上面代码实现了一次完整的兄弟之间的通信,具体过程如下所列。
  (1)首先在父组件Parent中定义两个兄弟组件Child,其中type属性为“p”的子组件用于递增,绑定了plus()方法;type属性为“m”的子组件用于递减,绑定了minus()方法。
  (2)然后在子组件Child中接收传递过来的digit属性和onDigitChange()方法,前者会作为文本框的值,后者会在事件处理程序onChange()中被调用。
  (3)如果在递增文本框中修改数值,那么就将新值传给plus()方法。递减文本框的处理过程与之类似,只是将plus()方法替换成minus()方法。
  (4)最后更新父组件中的两个状态:type和digit,完成信息的传递。
## 三、跨级通信
  在一棵组件树中,当多个组件需要跨级通信时,所处的层级越深,那么需要过渡的中间层就越多,完成一次通信将变得非常繁琐,而在数据传递过程中那些作为桥梁的组件,其代码也将变得冗余且臃肿。
  在React中,还可用Context实现跨级通信。Context能存放组件树中需要全局共享的数据,也就是说,一个组件可以借助Context跨越层级直接将数据传递给它的后代组件。如图8所示,左边的数据会通过组件的props逐级显式地传递,右边的数据会通过Context让所有组件都可访问。
:-: ![](https://img.kancloud.cn/d2/24/d2242b886e85a235140fa9494bf47b03_1300x642.png =500x)
图8 props和context
  随着React v16.3的发布,引入了一种全新的Context,修正了旧版本中较为棘手的问题,接下来的篇幅将着重分析这两个版本的Context。
**1)旧的Context**
  在旧版本的Context中,首先要在顶层组件内添加getChildContext()方法和静态属性childContextTypes,前者用于生成一个context对象(即初始化Context需要携带的数据),后者通过[prop-types库](https://www.cnblogs.com/strick/p/10569909.html)限制该对象的属性的数据类型,两者缺一不可。在下面的示例中,Grandpa是顶层组件,Son是中间组件,要传递的是一个包含name属性的对象。
~~~js
//顶层组件
class Grandpa extends React.Component {
getChildContext() {
return { name: "strick" };
}
render() {
return <Son />;
}
}
Grandpa.childContextTypes = {
name: PropTypes.string
};
//中间组件
class Son extends React.Component {
render() {
return <Grandson />;
}
}
~~~
  然后给后代组件(例如下面的Grandson)添加静态属性contextTypes,限制要接收的属性的数据类型,最后就能通过读取this.context得到由顶层组件提供的数据。
~~~js
class Grandson extends React.Component {
render() {
return <p>{this.context.name}</p>;
}
}
Grandson.contextTypes = {
name: PropTypes.string
};
~~~
  从上面的示例中可以看出,跨级通信的准备工作并不简单,需要在两处做不同的配置。React官方建议慎用旧版的Context,因为它相当于JavaScript中的全局变量,容易造成数据流混乱、重名覆盖等各种副作用,并且在未来的React版本中有可能被废弃。
  虽然在功能上Context实现了跨级通信,但本质上数据还是像props一样逐级传递的,因此如果某个中间组件的shouldComponentUpdate()方法返回false的话,就会阻止下层的组件更新Context中的数据。接下来会演示这个致命的缺陷,沿用上一个示例,对两个组件做些调整。在Grandpa组件中,先让Context保存组件的name状态,再新增一个按钮,并为其注册一个能更新组件状态的点击事件;在Son组件中,添加shouldComponentUpdate()方法,它的返回值是false。在把Grandpa组件挂载到DOM中后,点击按钮就能发现Context的更新传播终止于Son组件。
~~~js
class Grandpa extends React.Component {
constructor(props) {
super(props);
this.state = { name: "strick" };
this.click = this.click.bind(this);
}
getChildContext() {
return { name: this.state.name };
}
click() {
this.setState({ name: "freedom" });
}
render() {
return (
<>
<Son />
<button onClick={this.click}>提交</button>
</>
);
}
}
class Son extends React.Component {
shouldComponentUpdate() {
return false;
}
render() {
return <Grandson />;
}
}
~~~
**2)新的Context**
  这个版本的Context不仅采用了更符合React风格的声明式写法,还可以直接将数据传递给后代组件而不用逐级传递,一举冲破了shouldComponentUpdate()方法的限制。下面仍然使用上一节的三个组件,完成一次新的跨级通信。
~~~js
const NameContext = React.createContext({name: "strick"});
class Grandpa extends React.Component {
render() {
return (
<NameContext.Provider value={{name: "freedom"}}>
<Son />
</NameContext.Provider>
);
}
}
class Son extends React.Component {
render() {
return <Grandson />;
}
}
class Grandson extends React.Component {
render() {
return (
<NameContext.Consumer>{context => <p>{context.name}</p>}</NameContext.Consumer>
);
}
}
~~~
  通过上述代码可知,新的Context由三部分组成:
  (1)React.createContext()方法,接收一个可选的defaultValue参数,返回一个Context对象(例如NameContext),包含两个属性:Provider和Consumer,它们是一对相呼应的组件。
  (2)Provider,来源组件,它的value属性就是要传送的数据,Provider可关联多个来自于同一个Context对象的Consumer,像NameContext.Provider只能与NameContext.Consumer配合使用。
  (3)Consumer,目标组件,出现在Provider之后,可接收一个返回React元素的函数,如果Consumer能找到对应的Provider,那么函数的参数就是Provider的value属性,否则就读取defaultValue的值。
  注意,Provider组件会通过Object.is()对其value属性的新旧值做比较,以此确定是否更新作为它后代的Consumer组件。
## 四、无级通信
  当两个没有嵌套关系(即无级)的组件需要通信时,可以借助消息队列实现。下面是一个用观察者模式实现的简易消息队列库,其处理过程类似于事件系统,如果将消息看成事件,那么订阅消息就是绑定事件,而发布消息就是触发事件。
~~~js
class EventEmitter {
constructor() {
this.events = {};
}
sub(event, listener) { //订阅消息
if (!this.events[event]) {
this.events[event] = { listeners: [] };
}
this.events[event].listeners.push(listener);
}
pub(name, ...params) { //发布消息
for (const listener of this.events[name].listeners) {
listener.apply(this, params);
}
}
}
~~~
  EventEmitter只包含了三个方法,它们的功能如下所列:
  (1)构造函数,初始化了一个用于缓存各类消息的容器。
  (2)sub()方法,将回调函数用消息名称分类保存。
  (3)pub()方法,依次执行了指定名称下的消息集合。
  下面用一个示例演示无级通信,在Sub组件的构造函数中,会订阅一次消息,消息名称为"TextBox",回调函数会接收一个参数,并将其输出到控制台。
~~~js
let emitter = new EventEmitter();
class Sub extends React.Component {
constructor(props) {
super(props);
emitter.sub("TextBox", value => console.log(value));
}
render() {
return <p>订阅消息</p>;
}
}
~~~
  在下面的Pub组件中,为文本框注册了onChange事件,在事件处理程序handle()中发布名为"TextBox"的消息集合,并将文本框中的值作为参数传递到回调函数中。
~~~js
class Pub extends React.Component {
constructor(props) {
super(props);
this.state = { value: "" };
}
handle(e) {
const value = e.target.value;
emitter.pub("TextBox", value);
this.setState({ value });
}
render() {
return <input value={this.state.value} onChange={this.handle.bind(this)} />;
}
}
~~~
  Sub组件和Pub组件会像下面这样,以兄弟的关系挂载到DOM中。当修改文本框中的内容时,就会触发消息的发布,从而完成了一次它们之间的通信。
~~~js
ReactDOM.render(
<>
<Sub />
<Pub />
</>,
document.getElementById("container")
);
~~~
  当业务逻辑复杂到一定程度时,普通的消息队列可能就捉襟见肘了,此时可以考虑引入Mobx、Redux等专门的状态管理工具来实现组件之间的通信。
*****
> 原文出处:
[博客园-React躬行记](https://www.cnblogs.com/strick/category/1455720.html)
[知乎专栏-React躬行记](https://zhuanlan.zhihu.com/pwreact)
已建立一个微信前端交流群,如要进群,请先加微信号freedom20180706或扫描下面的二维码,请求中需注明“看云加群”,在通过请求后就会把你拉进来。还搜集整理了一套[面试资料](https://github.com/pwstrick/daily),欢迎浏览。
![](https://box.kancloud.cn/2e1f8ecf9512ecdd2fcaae8250e7d48a_430x430.jpg =200x200)
推荐一款前端监控脚本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不仅能监控前端的错误、通信、打印等行为,还能计算各类性能参数,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、扩展运算符和剩余参数
- 3、解构
- 4、模板字面量
- 5、对象字面量的扩展
- 6、Symbol
- 7、代码模块化
- 8、数字
- 9、字符串
- 10、正则表达式
- 11、对象
- 12、数组
- 13、类型化数组
- 14、函数
- 15、箭头函数和尾调用优化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、类
- 21、类的继承
- 22、Promise
- 23、Promise的静态方法和应用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基础实践
- 3、WebRTC视频通话
- 4、Web音视频基础
- CSS进阶
- 1、CSS基础拾遗
- 2、伪类和伪元素
- 3、CSS属性拾遗
- 4、浮动形状
- 5、渐变
- 6、滤镜
- 7、合成
- 8、裁剪和遮罩
- 9、网格布局
- 10、CSS方法论
- 11、管理后台响应式改造
- React
- 1、函数式编程
- 2、JSX
- 3、组件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表单
- 8、样式
- 9、组件通信
- 10、高阶组件
- 11、Redux基础
- 12、Redux中间件
- 13、React Router
- 14、测试框架
- 15、React Hooks
- 16、React源码分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基础
- 4、webpack进阶
- 5、Git
- 6、Fiddler
- 7、自制脚手架
- 8、VSCode插件研发
- 9、WebView中的页面调试方法
- Vue.js
- 1、数据绑定
- 2、指令
- 3、样式和表单
- 4、组件
- 5、组件通信
- 6、内容分发
- 7、渲染函数和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、数据类型
- 2、接口
- 3、类
- 4、泛型
- 5、类型兼容性
- 6、高级类型
- 7、命名空间
- 8、装饰器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系统和网络
- 3、命令行工具
- 4、自建前端监控系统
- 5、定时任务的调试
- 6、自制短链系统
- 7、定时任务的进化史
- 8、通用接口
- 9、微前端实践
- 10、接口日志查询
- 11、E2E测试
- 12、BFF
- 13、MySQL归档
- 14、压力测试
- 15、活动规则引擎
- 16、活动配置化
- 17、UmiJS版本升级
- 18、半吊子的可视化搭建系统
- 19、KOA源码分析(上)
- 20、KOA源码分析(下)
- 21、花10分钟入门Node.js
- 22、Node环境升级日志
- 23、Worker threads
- 24、低代码
- 25、Web自动化测试
- 26、接口拦截和页面回放实验
- 27、接口管理
- 28、Cypress自动化测试实践
- 29、基于Electron的开播助手
- Node.js精进
- 1、模块化
- 2、异步编程
- 3、流
- 4、事件触发器
- 5、HTTP
- 6、文件
- 7、日志
- 8、错误处理
- 9、性能监控(上)
- 10、性能监控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 监控系统
- 1、SDK
- 2、存储和分析
- 3、性能监控
- 4、内存泄漏
- 5、小程序
- 6、较长的白屏时间
- 7、页面奔溃
- 8、shin-monitor源码分析
- 前端性能精进
- 1、优化方法论之测量
- 2、优化方法论之分析
- 3、浏览器之图像
- 4、浏览器之呈现
- 5、浏览器之JavaScript
- 6、网络
- 7、构建
- 前端体验优化
- 1、概述
- 2、基建
- 3、后端
- 4、数据
- 5、后台
- Web优化
- 1、CSS优化
- 2、JavaScript优化
- 3、图像和网络
- 4、用户体验和工具
- 5、网站优化
- 6、优化闭环实践
- 数据结构与算法
- 1、链表
- 2、栈、队列、散列表和位运算
- 3、二叉树
- 4、二分查找
- 5、回溯算法
- 6、贪心算法
- 7、分治算法
- 8、动态规划
- 程序员之路
- 大学
- 2011年
- 2012年
- 2013年
- 2014年
- 项目反思
- 前端基础学习分享
- 2015年
- 再一次项目反思
- 然并卵
- PC网站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端学习之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 日志
- 2020