react組件跨層通訊 筆記

react組件跨層通訊 筆記

組件與組件之間的關係,大體可分爲 4 種。

  1. 父與子:父組件包裹子組件,父組件向子組件傳遞數據。
  2. 子與父:子組件存在於父組件之中,子組件須要向父組件傳遞數據。
  3. 兄弟:兩個組件並列存在於父組件中,須要金屬數據進行相互傳遞。
  4. 無直接關係:兩個組件並無直接的關聯關係,處在一棵樹中相距甚遠的位置,但須要共享、傳遞數據。

通訊方式總結以下:

imagejavascript

父與子

父與子的通訊主要是經過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} />
            </>
          />
        )
    }

兄弟

兄弟組件之間的通訊,每每依賴共同的父組件進行中轉。也就是狀態提高。數組

無直接關係

無直接關係就是兩個組件的直接關聯性並不大,它們身處於多層級的嵌套關係中,既不是父子關係,也不相鄰,而且相對遙遠。他們以前通訊的方式有:框架

  1. Context,即React 的 Context API。
  2. 全局變量與事件(不太推薦)。全局變量,顧名思義就是放在 Window 上的變量。但值得注意的是修改 Window 上的變量並不會引發 React 組件從新渲染。
  3. 狀態管理框架。狀態管理框架提供了很是豐富的解決方案,常見的有 Flux、Redux 及 Mobx。
  4. 「發佈-訂閱」模式

「發佈-訂閱」模式

「發佈-訂閱」模式可謂是解決通訊類問題的「萬金油」,使用發佈-訂閱模式的優勢在於,監聽事件的位置和觸發事件的位置是不受限的,就算相隔十萬八千里,只要它們在同一個上下文裏,就可以彼此感知。這個特性,太適合用來應對「任意組件通訊」這種場景了。ide

發佈-訂閱模型 API 設計思路

  • on():負責註冊事件的監聽器,指定事件觸發時的回調函數。
  • emit():負責觸發事件,能夠經過傳參使其在觸發的時候攜帶數據 。
  • off():負責監聽器的刪除。

發佈-訂閱模型編碼實現(重要,面試考點)

「發佈-訂閱」模式不只在應用層面十分受歡迎,它更是面試官的心頭好。在涉及設計模式的面試中,若是隻容許出一道題,那麼我相信大多數的面試官都會和我同樣,會堅決果斷地選擇考察「發佈-訂閱模式的實現」。函數

在寫代碼以前,先要捋清楚思路。這裏我把「實現 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,多年來一直是面試的大熱點,務必要好好把握。

這個發佈-訂閱模式是我買的專欄裏講的,我覺講的比較好,就直接拿過來了,我以爲老師的功底仍是挺深厚的,就是課程數量有點少,感受把有些內容拿出來精講一下就行了。下面的二維碼就是課程,有須要的同窗能夠本身買來看看。
image

相關文章
相關標籤/搜索