React 組件間通訊
說 React 組件間通訊之前,我們先來討論一下 React 組件究竟有多少種層級間的關系。假設我們開發的項目是一個純 React 的項目,那我們項目應該有如下類似的關系:
父子:Parent 與 Child_1、Child_2、Child_1_1、Child_1_2、Child_2_1
兄弟:Child_1 與 Child_2、Child_1_1 與 Child_2、etc.
針對這些關系,我們將來好好討論一下這些關系間的通訊方式。
(在 React 中,React 組件之間的關系為從屬關系,與 DOM 元素之間的父子關系有所不同,下面只是為了說明方便,將 React 組件的關系類比成父子關系進行闡述)
父組件向子組件通訊
通訊是單向的,數據必須是由一方傳到另一方。在 React 中,父組件可以向子組件通過傳 props 的方式,向子組件進行通訊。
class Parent extends Component{ |
如果父組件與子組件之間不止一個層級,如 Parent 與 Child_1_1 這樣的關系,可通過 ... 運算符
(Object 剩余和展開屬性),將父組件的信息,以更簡潔的方式傳遞給更深層級的子組件。通過這種方式,不用考慮性能的問題,通過 babel 轉義后的 ... 運算符
性能和原生的一致,且上級組件 props 與 state 的改變,會導致組件本身及其子組件的生命周期改變,
// 通過 ... 運算符 向 Child_1_1 傳遞 Parent 組件的信息 |
子組件向父組件通訊
在上一個例子中,父組件可以通過傳遞 props 的方式,自頂而下向子組件進行通訊。而子組件向父組件通訊,同樣也需要父組件向子組件傳遞 props 進行通訊,只是父組件傳遞的,是作用域為父組件自身的函數,子組件調用該函數,將子組件想要傳遞的信息,作為參數,傳遞到父組件的作用域中。
class Parent extends Component{ |
在上面的例子中,我們使用了 箭頭函數
,將父組件的 transferMsg 函數通過 props 傳遞給子組件,得益於箭頭函數,保證子組件在調用 transferMsg 函數時,其內部 this
仍指向父組件。
當然,對於層級比較深的子組件與父組件之間的通訊,仍可使用 ... 運算符
,將父組件的調用函數傳遞給子組件,具體方法和上面的例子類似。
兄弟組件間通訊
對於沒有直接關聯關系的兩個節點,就如 Child_1 與 Child_2 之間的關系,他們唯一的關聯點,就是擁有相同的父組件。參考之前介紹的兩種關系的通訊方式,如果我們向由 Child_1 向 Child_2 進行通訊,我們可以先通過 Child_1 向 Parent 組件進行通訊,再由 Parent 向 Child_2 組件進行通訊,所以有以下代碼。
class Parent extends Component{ |
然而,這個方法有一個問題,由於 Parent 的 state 發生變化,會觸發 Parent 及從屬於 Parent 的子組件的生命周期,所以我們在控制台中可以看到,在各個組件中的 componentDidUpdate 方法均被觸發。
有沒有更好的解決方式來進行兄弟組件間的通訊,甚至是父子組件層級較深的通訊的呢?
觀察者模式
在傳統的前端解耦方面,觀察者模式作為比較常見一種設計模式,大量使用在各種框架類庫的設計當中。即使我們在寫 React,在寫 JSX,我們核心的部分還是 JavaScript。
觀察者模式也叫 發布者-訂閱者模式
,發布者發布事件,訂閱者監聽事件並做出反應,對於上面的代碼,我們引入一個小模塊,使用觀察者模式進行改造。
import eventProxy from '../eventProxy' |
我們在 child_2 組件的 componentDidMount 中訂閱了 msg
事件,並在 child_1 componentDidMount 中,在 1s 后發布了 msg
事件,child_2 組件對 msg
事件做出相應,更新了自身的 state,我們可以看到,由於在整個通訊過程中,只改變了 child_2 的 state,因而只有 child_2 和 child_2_1 出發了一次更新的生命周期。
而上面代碼中,神奇的 eventProxy.js 究竟是怎樣的一回事呢?
// eventProxy.js |
eventProxy 中,總共有 on、one、off、trigger 這 4 個函數:
- on、one:on 與 one 函數用於訂閱者監聽相應的事件,並將事件響應時的函數作為參數,on 與 one 的唯一區別就是,使用 one 進行訂閱的函數,只會觸發一次,而 使用 on 進行訂閱的函數,每次事件發生相應時都會被觸發。
- trigger:trigger 用於發布者發布事件,將除第一參數(事件名)的其他參數,作為新的參數,觸發使用 one 與 on 進行訂閱的函數。
- off:用於解除所有訂閱了某個事件的所有函數。
Flux 與 Redux
Flux 作為 Facebook 發布的一種應用架構,他本身是一種模式,而不是一種框架,基於這個應用架構模式,在開源社區上產生了眾多框架,其中最受歡迎的就是我們即將要說的 Redux。更多關於 Flux 和 Redux 的介紹這里就不一一展開,有興趣的同學可以好好看看 Flux 官方介紹、Flux 架構入門教程–阮一峰等相關資料。
下面將來好好聊聊 Redux 在組件間通訊的方式。
Flux 需要四大部分組成:Dispatcher、Stores、Views/Controller-Views、Actions,其中的 Views/Controller-Views 可以理解為我們上面所說的 Parent 組件,其作用是從 state 當中獲取到相應的數據,並將其傳遞給他的子組件(descendants)。而另外 3 個部分,則是由 Redux 來提供了。
// 該例子主要對各組件的 componentDidMount 進行改造,其余部分一致 |
在上面的例子中,我們將一個名為 reducer
的函數作為參數,生成我們所需要的 store,reducer 接受兩個參數,一個是存儲在 store 里面的 state,另一個是每一次調用 dispatch 所傳進來的 action。reducer 的作用,就是對 dispatch 傳進來的 action 進行處理,並將結果返回。而里面的 state 可以通過 store 里面的 getState 方法進行獲得,其結果與最后一次通過 reducer 處理后的結果保持一致。
在 child_1 組件中,我們每隔 1s 通過 store 的 dispatch 方法,向 store 傳入包含有 type 字段的 action,reducer 直接將 action 進行返回。
而在 child_2 與 child_2_1 組件中,通過 store 的 subscribe 方法,監聽 store 的變化,觸發 dispatch 后,所有通過 subscribe 進行監聽的函數都會作出相應,根據當前通過 store.getState() 獲取到的結果進行處理,對當前組件的 state 進行設置。所以我們可以在控制台上看到各個組件更新及存儲在 store 中 state 的情況:
在 Redux 中,store 的作用,與 MVC 中的 Model 類似,可以將我們項目中的數據傳遞給 store,交給 store 進行處理,並可以實時通過 store.getState() 獲取到存儲在 store 中的數據。我們對上面例子的 reducer 及各個組件的 componentDidMount 做點小修改,看看 store 的這一個特性。
import {createStore} from 'redux' |
我們對創建 store 時所傳進去的 reducer 進行修改。reducer 中,其參數 state 為當前 store 的值,我們對不同的 action 進行處理,並將處理后的結果存儲在 state 中並進行返回。此時,通過 store.getState() 獲取到的,就是我們處理完成后的 state。
Redux 內部的實現,其實也是基於觀察者模式的,reducer 的調用結果,存儲在 store 內部的 state 中,並在每一次 reducer 的調用中並作為參數傳入。所以在 child_1 組件第 2s 的 dispatch 后,child_2 與 child_2_1 組件通過 subscribe 監聽的函數,其通過 getState 獲得的值,都包含有 child_2 與 child_2_1 字段的,這就是為什么第 2s 后的響應,child_2 也進行了一次生命周期。所以在對 subscribe 響應后的處理,最好還是先校對通過 getState() 獲取到的 state 與當前組件的 state 是否相同。
// child_2 |
加上這樣的校驗,各個組件的生命周期的觸發就符合我們的預期了。
小結
Redux 對於組件間的解耦提供了很大的便利,如果你在考慮該不該使用 Redux 的時候,社區里有一句話說,“當你不知道該不該使用 Redux 的時候,那就是不需要的”。Redux 用起來一時爽,重構或者將項目留給后人的時候,就是個大坑,Redux 中的 dispatch 和 subscribe 方法遍布代碼的每一個角落。剛剛的例子不是最好的,Flux 設計中的 Controller-Views 概念就是為了解決這個問題出發的,將所有的 subscribe 都置於 Parent 組件(Controller-Views),由最上層組件控制下層組件的表現,然而,這不就是我們所說的 子組件向父組件通訊
這種方式了。