通訊方式總結以下:
javascript
父與子的通訊主要是經過props。React 開發的每一個組件都在使用這樣的設計模式。每一個組件都會在父級被使用,再傳入 Props,完成信息的傳遞。這樣的交互方式儘管不起眼,容易讓人忽略,但正是最經典的設計。java
子與父的通訊主要依賴回調函數。react
回調函數在 JavaScript 中稱爲 callback。React 在設計中沿用了 JavaScript 的經典設計,容許函數做爲參數賦值給子組件。最基礎的用法就像下面的例子同樣,經過包裝傳遞 text 的值。面試
class Child extends React.Component { handleChanged = (e) => { //調用父組件傳進來的回調函數 this.props.onChangeText(e.target.text) } render() { return <input onChange={handleTextChanged} /> } } class Father extends React.Component { handleTextChanged = (text) => { console.log(text) } render() { return ( // 把函數當作props參數傳給子組件 <Child onChangeText={this.handleTextChanged} /> ) } }
須要注意的是,實例函數是一種不被推薦的使用方式。這種通訊方式常見於 React 流行初期,那時有不少組件都經過封裝 jQuery 插件生成。最多見的一種狀況是在 Modal 中使用這種方式。以下代碼所示:設計模式
import React from 'react' class HomePage extends React.Component { modalRef = React.createRef() showModal = () ={ this.modalRef.show() } hideModal = () => { //經過ref獲取到的實例操做,不過如今通常都不這麼用了,如今會給一個參數show=true或show=false來控制組件顯示或者隱藏 this.modalRef.hide(); } render() { const { text } = this.state return ( <> <Button onClick={this.showModal}>展現 Modal </Button> <Button onClick={this.hideModal}>隱藏 Modal </Button> <Modal ref={modalRef} /> </> /> ) }
兄弟組件之間的通訊,每每依賴共同的父組件進行中轉。也就是狀態提高。數組
無直接關係就是兩個組件的直接關聯性並不大,它們身處於多層級的嵌套關係中,既不是父子關係,也不相鄰,而且相對遙遠。他們以前通訊的方式有:框架
「發佈-訂閱」模式可謂是解決通訊類問題的「萬金油」,使用發佈-訂閱模式的優勢在於,監聽事件的位置和觸發事件的位置是不受限的,就算相隔十萬八千里,只要它們在同一個上下文裏,就可以彼此感知。這個特性,太適合用來應對「任意組件通訊」這種場景了。ide
「發佈-訂閱」模式不只在應用層面十分受歡迎,它更是面試官的心頭好。在涉及設計模式的面試中,若是隻容許出一道題,那麼我相信大多數的面試官都會和我同樣,會堅決果斷地選擇考察「發佈-訂閱模式的實現」。函數
在寫代碼以前,先要捋清楚思路。這裏我把「實現 EventEmitter」這個大問題,拆解爲 3 個具體的小問題,下面咱們逐個來解決。測試
提到「對應關係」,應該聯想到的是「映射」。在 JavaScript 中,處理「映射」咱們大部分狀況下都是用對象來作的。因此說在全局咱們須要設置一個對象,來存儲事件和監聽函數之間的關係:
constructor() { // eventMap 用來存儲事件和監聽函數之間的關係 this.eventMap= {} }
所謂「訂閱」,也就是註冊事件監聽函數的過程。這是一個「寫」操做,具體來講就是把事件和對應的監聽函數寫入到 eventMap 裏面去:
// type 這裏就表明事件的名稱 on(type, handler) { // hanlder 必須是一個函數,若是不是直接報錯 if(!(handler instanceof Function)) { throw new Error("哥 你錯了 請傳一個函數") } // 判斷 type 事件對應的隊列是否存在 if(!this.eventMap[type]) { // 若不存在,新建該隊列 this.eventMap[type] = [] } // 若存在,直接往隊列裏推入 handler this.eventMap[type].push(handler) }
訂閱操做是一個「寫」操做,相應的,發佈操做就是一個「讀」操做。發佈的本質是觸發安裝在某個事件上的監聽函數,咱們須要作的就是找到這個事件對應的監聽函數隊列,將隊列中的 handler 依次執行出隊:
// 別忘了咱們前面說過觸發時是能夠攜帶數據的,params 就是數據的載體 emit(type, params) { // 假設該事件是有訂閱的(對應的事件隊列存在) if(this.eventMap[type]) { // 將事件隊列裏的 handler 依次執行出隊 this.eventMap[type].forEach((handler, index)=> { // 注意別忘了讀取 params handler(params) }) } }
到這裏,最最關鍵的 on 方法和 emit 方法就實現完畢了。最後咱們補充一個 off 方法:
// 監聽器的刪除 /* >>> 是無符號按位右移運算符。考慮 indexOf 返回-1 的狀況:splice方法喜歡把-1解讀爲當前數組的最後 一個元素,這樣子的話,在壓根沒有對應函數能夠刪的狀況下,無論三七二十一就把最後一個元素給幹掉了。 而 >>> 符號對正整數沒有影響,但對於-1來講它會把-1轉換爲一個巨大的數(你能夠本地運行下試試看, 應該是一個32位全是1的二進制數,折算成十進制就是 4294967295)。這個巨大的索引splice是找不到的, 找不到就不刪,因而一切保持原狀,恰好符合咱們的預期。 */ off(type, handler) { if(this.eventMap[type]) { this.eventMap[type].splice(this.eventMap[type].indexOf(handler)>>>0,1) } }
接着把這些代碼片斷拼接進一個 class 裏面,一個核心功能完備的 EventEmitter 就完成啦:
class myEventEmitter { constructor() { // eventMap 用來存儲事件和監聽函數之間的關係 this.eventMap = {}; } // type 這裏就表明事件的名稱 on(type, handler) { // hanlder 必須是一個函數,若是不是直接報錯 if (!(handler instanceof Function)) { throw new Error("哥 你錯了 請傳一個函數"); } // 判斷 type 事件對應的隊列是否存在 if (!this.eventMap[type]) { // 若不存在,新建該隊列 this.eventMap[type] = []; } // 若存在,直接往隊列裏推入 handler this.eventMap[type].push(handler); } // 別忘了咱們前面說過觸發時是能夠攜帶數據的,params 就是數據的載體 emit(type, params) { // 假設該事件是有訂閱的(對應的事件隊列存在) if (this.eventMap[type]) { // 將事件隊列裏的 handler 依次執行出隊 this.eventMap[type].forEach((handler, index) => { // 注意別忘了讀取 params handler(params); }); } } // 監聽器的刪除 /* >>> 是無符號按位右移運算符。考慮 indexOf 返回-1 的狀況:splice方法喜歡把-1解讀爲當前數組的最後 一個元素,這樣子的話,在壓根沒有對應函數能夠刪的狀況下,無論三七二十一就把最後一個元素給幹掉了。 而 >>> 符號對正整數沒有影響,但對於-1來講它會把-1轉換爲一個巨大的數(你能夠本地運行下試試看, 應該是一個32位全是1的二進制數,折算成十進制就是 4294967295)。這個巨大的索引splice是找不到的, 找不到就不刪,因而一切保持原狀,恰好符合咱們的預期。 */ off(type, handler) { if (this.eventMap[type]) { this.eventMap[type].splice(this.eventMap[type].indexOf(handler) >>> 0, 1); } } }
下面咱們對 myEventEmitter 進行一個簡單的測試,建立一個 myEvent 對象做爲 myEventEmitter 的實例,而後針對名爲 「test」 的事件進行監聽和觸發:
// 實例化 myEventEmitter const myEvent = new myEventEmitter(); // 編寫一個簡單的 handler const testHandler = function (params) { console.log(`test事件被觸發了,testHandler 接收到的入參是${params}`); }; // 監聽 test 事件 myEvent.on("test", testHandler); // 在觸發 test 事件的同時,傳入但願 testHandler 感知的參數 myEvent.emit("test", "newState");
如今你能夠試想一下,對於任意的兩個組件 A 和 B,假如我但願實現雙方之間的通訊,藉助 EventEmitter 來作就很簡單了,以數據從 A 流向 B 爲例。
咱們能夠在 B 中編寫一個handler(記得將這個 handler 的 this 綁到 B 身上),在這個 handler 中進行以 B 爲上下文的 this.setState 操做,而後將這個 handler 做爲監聽器與某個事件關聯起來。好比這樣:
// 注意這個 myEvent 是提早實例化並掛載到全局的,此處再也不重複示範實例化過程 const globalEvent = window.myEvent class B extends React.Component { // 這裏省略掉其餘業務邏輯 state = { newParams: "" }; handler = (params) => { this.setState({ newParams: params }); }; bindHandler = () => { globalEvent.on("someEvent", this.handler); }; render() { return ( <div> <button onClick={this.bindHandler}>點我監聽A的動做</button> <div>A傳入的內容是[{this.state.newParams}]</div> </div> ); } }
接下來在 A 組件中,只須要直接觸發對應的事件,而後將但願攜帶給 B 的數據做爲入參傳遞給 emit 方法便可。代碼以下:
class A extends React.Component { // 這裏省略掉其餘業務邏輯 state = { infoToB: "哈哈哈哈我來自A" }; reportToB = () => { // globalEvent從全局對象window獲取 // 這裏的 infoToB 表示 A 自身狀態中須要讓 B 感知的那部分數據 globalEvent.emit("someEvent", this.state.infoToB); }; render() { return <button onClick={this.reportToB}>點我把state傳遞給B</button>; } }
如此一來,便可以實現 A 到 B 的通訊了。這裏我將 A 與 B 編排爲兄弟組件,代碼以下:
export default function App() { return ( <div className="App"> <B /> <A /> </div> ); }
你須要把重點放在對編碼的實現和理解上,尤爲是基於「發佈-訂閱」模式實現的 EventEmitter,多年來一直是面試的大熱點,務必要好好把握。
這個發佈-訂閱模式是我買的專欄裏講的,我覺講的比較好,就直接拿過來了,我以爲老師的功底仍是挺深厚的,就是課程數量有點少,感受把有些內容拿出來精講一下就行了。下面的二維碼就是課程,有須要的同窗能夠本身買來看看。