根據組件之間的嵌套關系(即層級關系)可分為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都是兄弟關系。
:-: 
:-: 圖6 組件樹
  兄弟之間不能直接通信,需要借助狀態提升的方式間接實現信息的傳遞,即把組件之間要共享的狀態提升至最近的父組件中,由父組件來統一管理。而任意一個兄弟組件可通過從父組件傳來的回調函數更新共享狀態,新的共享狀態再通過父組件的props回傳給子組件,從而完成一次兄弟之間的通信。在下面的例子中,會有兩個文本框(如圖7所示),當向其中一個輸入數字時,鄰近的文本框會隨之改變,要么加一,要么減一。
:-: 
圖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讓所有組件都可訪問。
:-: 
圖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),歡迎瀏覽。

推薦一款前端監控腳本:[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年
- 2024年
- 日志
- 2020