在React中最小的邏輯單元是組件,組件之間若是有耦合關係就會進行通訊,本文將會介紹React中的組件通訊的不一樣方式javascript
經過概括範,能夠將任意組件間的通訊歸類爲四種類型的組件間通訊,分別是父子組件,爺孫組件,兄弟組件和任意組件, 須要注意的是前三個也能夠算做任意組件的範疇,因此最後一個是萬能方法html
父子組件間的通訊分爲父組件向子組件通訊和子組件向父組件通訊兩種狀況,下面先來介紹父組件向子組件通訊, 傳統作法分爲兩種狀況,分別是初始化時的參數傳遞和實例階段的方法調用,例子以下前端
class Child { constructor(name) { // 獲取dom引用 this.$div = document.querySelector('#wp'); // 初始化時傳入name this.updateName(name); } updateName(name) { // 對外提供更新的api this.name = name; // 更新dom this.$div.innerHTML = name; } } class Parent { constructor() { // 初始化階段 this.child = new Child('yan'); setTimeout(() => { // 實例化階段 this.child.updateName('hou'); }, 2000); } } 複製代碼
在React中將兩個狀況統一處理,所有經過屬性來完成,之因此可以這樣是由於React在屬性更新時會自動從新渲染子組件, 下面的例子中,2秒後子組件會自動從新渲染,並獲取新的屬性值java
class Child extends Component { render() { return <div>{this.props.name}</div> } } class Parent extends Component { constructor() { // 初始化階段 this.state = {name: 'yan'}; setTimeout(() => { // 實例化階段 this.setState({name: 'hou'}) }, 2000); } render() { return <Child name={this.state.name} /> } } 複製代碼
下面來看一會兒組件如何向父組件通訊,傳統作法有兩種,一種是回調函數,另外一種是爲子組件部署消息接口react
先來看回調函數的例子,回調函數的優勢是很是簡單,缺點就是必須在初始化的時候傳入,而且不可撤回,而且只能傳入一個函數git
class Child { constructor(cb) { // 調用父組件傳入的回調函數,發送消息 setTimeout(() => { cb() }, 2000); } } class Parent { constructor() { // 初始化階段,傳入回調函數 this.child = new Child(function () { console.log('child update') }); } } 複製代碼
下面來看看消息接口方法,首先須要一個能夠發佈和訂閱消息的基類,好比下面實現了一個簡單的EventEimtter
,實際生產中能夠直接使用別人寫好的類庫,好比@jsmini/event,子組件繼承消息基類,就有了發佈消息的能力,而後父組件訂閱子組件的消息,便可實現子組件向父組件通訊的功能github
消息接口的優勢就是能夠隨處訂閱,而且能夠屢次訂閱,還能夠取消訂閱,缺點是略顯麻煩,須要引入消息基類api
// 消息接口,訂閱發佈模式,相似綁定事件,觸發事件 class EventEimtter { constructor() { this.eventMap = {}; } sub(name, cb) { const eventList = this.eventMap[name] = this.eventMap[name] || {}; eventList.push(cb); } pub(name, ...data) { (this.eventMap[name] || []).forEach(cb => cb(...data)); } } class Child extends EventEimtter { constructor() { super(); // 經過消息接口發佈消息 setTimeout(() => { this.pub('update') }, 2000); } } class Parent { constructor() { // 初始化階段,傳入回調函數 this.child = new Child(); // 訂閱子組件的消息 this.child.sub('update', function () { console.log('child update') }); } } 複製代碼
Backbone.js就同時支持回調函數和消息接口方式,但React中選擇了比較簡單的回調函數模式,下面來看一下React的例子markdown
class Child extends Component { constructor(props) { setTimeout(() => { this.props.cb() }, 2000); } render() { return <div></div> } } class Parent extends Component { render() { return <Child cb={() => {console.log('update')}} /> } } 複製代碼
父子組件其實能夠算是爺孫組件的一種特例,這裏的爺孫組件不光指爺爺和孫子,而是泛指祖先與後代組件通訊,可能隔着不少層級,咱們已經解決了父子組件通訊的問題,根據化歸法,很容易得出爺孫組件的答案,那就是層層傳遞屬性麼,把爺孫組件通訊分解爲多個父子組件通訊的問題前端工程師
層層傳遞的優勢是很是簡單,用已有知識就能解決,問題是會浪費不少代碼,很是繁瑣,中間做爲橋樑的組件會引入不少不屬於本身的屬性
在React中,經過context可讓祖先組件直接把屬性傳遞到後代組件,有點相似星際旅行中的蟲洞同樣,經過context這個特殊的橋樑,能夠跨越任意層次向後代組件傳遞消息
怎麼在須要通訊的組件之間開啓這個蟲洞呢?須要雙向聲明,也就是在祖先組件聲明屬性,並在後代組件上再次聲明屬性,而後在祖先組件上放上屬性就能夠了,就能夠在後代組件讀取屬性了,下面看一個例子
import PropTypes from 'prop-types'; class Child extends Component { // 後代組件聲明須要讀取context上的數據 static contextTypes = { text: PropTypes.string } render() { // 經過this.context 讀取context上的數據 return <div>{this.context.text}</div> } } class Ancestor extends Component { // 祖先組件聲明須要放入context上的數據 static childContextTypes = { text: PropTypes.string } // 祖先組件往context放入數據 getChildContext() { return {text: 'yanhaijing'} } } 複製代碼
context的優勢是能夠省去層層傳遞的麻煩,而且經過雙向聲明控制了數據的可見性,對於層數不少時,不失爲一種方案;但缺點也很明顯,就像全局變量同樣,若是不加節制很容易形成混亂,並且也容易出現重名覆蓋的問題
我的的建議是對一些全部組件共享的只讀信息能夠採用context來傳遞,好比登陸的用戶信息等
小貼士:React Router路由就是經過context來傳遞路由屬性的
若是兩個組件是兄弟關係,能夠經過父組件做爲橋樑,來讓兩個組件之間通訊,這其實就是主模塊模式
下面的例子中,兩個子組件經過父組件來實現顯示數字同步的功能
class Parent extends Component { constructor() { this.onChange = function (num) { this.setState({num}) }.bind(this); } render() { return ( <div> <Child1 num={this.state.num} onChange={this.onChange}> <Child2 num={this.state.num} onChange={this.onChange}> </div> ); } } 複製代碼
主模塊模式的優勢就是解耦,把兩個子組件之間的耦合關係,解耦成子組件和父組件之間的耦合,把分散的東西收集在一塊兒好處很是明顯,能帶來更好的可維護性和可擴展性
任意組件包括上面的三種關係組件,上面三種關係應該優先使用上面介紹的方法,對於任意的兩個組件間通訊,總共有三種辦法,分別是共同祖先法,消息中間件和狀態管理
基於咱們上面介紹的爺孫組件和兄弟組件,只要找到兩個組件的共同祖先,就能夠將任意組件之間的通訊,轉化爲任意組件和共同祖先之間的通訊,這個方法的好處就是很是簡單,已知知識就能搞定,缺點就是上面兩種模式缺點的疊加,除了臨時方案,不建議使用這種方法
另外一種比較經常使用的方法是消息中間件,就是引入一個全局消息工具,兩個組件經過這個全局工具進行通訊,這樣兩個組件間的通訊,就經過全局消息媒介完成了
還記得上面介紹的消息基類嗎?下面的例子中,組件1和組件2經過全局event進行通訊
class EventEimtter { constructor() { this.eventMap = {}; } sub(name, cb) { const eventList = this.eventMap[name] = this.eventMap[name] || {}; eventList.push(cb); } pub(name, ...data) { (this.eventMap[name] || []).forEach(cb => cb(...data)); } } // 全局消息工具 const event = new EventEimtter; // 一個組件 class Element1 extends Component { constructor() { // 訂閱消息 event.sub('element2update', () => {console.log('element2 update')}); } } // 另外一個組件。 class Element2 extends Component { constructor() { // 發佈消息 setTimeout(function () { event.pub('element2update') }, 2000) } } 複製代碼
消息中間件的模式很是簡單,利用了觀察者模式,將兩個組件之間的耦合解耦成了組件和消息中心+消息名稱的耦合,但爲了解耦卻引入全局消息中心和消息名稱,消息中心對組件的侵入性很強,和第三方組件通訊不能使用這種方式
小型項目比較適合使用這種方式,但隨着項目規模的擴大,達到中等項目之後,消息名字爆炸式增加,消息名字的維護成了棘手的問題,重名機率極大,沒有人敢隨便刪除消息信息,消息的發佈者找不到消息訂閱者的信息等
其實上面的問題也不是沒有解決辦法,重名的問題能夠經過制定規範,消息命名空間等方式來極大下降衝突,其餘問題能夠經過把消息名字統一維護到一個文件,經過對消息的中心化管理,可讓不少問題都很容易解決
若是你的項目很是大,上面兩種方案都不合適,那你可能須要一個狀態管理工具,經過狀態管理工具把組件之間的關係,和關係的處理邏輯從組建中抽象出來,並集中化到統一的地方來處理,Redux就是一個很是不錯的狀態管理工具
除了Redux,還有Mobx,Rematch,reselect等工具,本文不展開介紹,有機會後面單獨成文,這些都是用來解決不一樣問題的,只要根據本身的場景選擇合適的工具就行了
組件間的關係變幻無窮,均可以用上面介紹的方法解決,對於不一樣規模的項目,應該選擇適合本身的技術方案,上面介紹的不一樣方式解耦的程度是不同的,關於不一樣耦合關係的好壞,能夠看我以前的文章《圖解7種耦合關係》
本文節選自個人新書《React 狀態管理與同構實戰》,感興趣的同窗能夠繼續閱讀本書,這本書由我和前端自身技術侯策協力打磨,凝結了咱們在學習、實踐 React 框架過程當中的積累和心得。除了 React 框架使用介紹之外,着重剖析了狀態管理以及服務端渲染同構應用方面的內容。同時吸收了社區大量優秀思想,進行概括比對。
本書受到百度公司副總裁沈抖、百度高級前端工程師董睿,以及知名JavaScript語言專家阮一峯、Node.js佈道者狼叔、Flarum中文社區創始人 justjavac、新浪移動前端技術專家小爝、知乎知名博主顧軼靈等前端圈衆多專家大咖的聯協力薦。
有興趣的讀者能夠點擊下面的連接購買,再次感謝各位的支持與鼓勵!懇請各位批評指正!