React填坑記(一):組件通信

項目重構中碰到了很多組件通信帶來的問題,對組件通信進行總結。

react組件通信有幾種常見的場景

  • 父子組件通信(Demo1):父子組件通信最為簡單,父組件向子組件傳遞props,子組件接受父組件的回調
  • 跨級組件通信(Demo2):(使用Context,典型的是Redux)
  • 兄弟組件通信(Demo3) :在父組件維護state,兄弟組件接受props,兄弟組件修改父組件state。
  • 全局事件通信(Demo4):全局事件需要保證派發事件時,監聽者必須存在,否則可能會導致監聽不到事件的發生,兩種解決方式,1.存儲發布的事件,當有訂閱者訂閱時執行存儲的已發布事件,或者監聽DomContentLoaded之後再進行事件派發,保證監聽者已經存在。

以上是幾種最基本的情形,但現實中還是可能存在種種問題。

UI渲染不同步

要把數據同步到UI總共分兩步

  1. 觸發組件的render方法
  2. render方法根據最新的props,state更新UI

如果發現UI和數據不同步,則可能是1.沒有觸發render,2.render方法里的渲染的數據源不是最新的

觸發render的條件

  1. First Render: 首次渲染不會進行SCU(ShouldComponentUpdate)檢查,肯定會render
  2. forceUpdate:會跳過SCU檢查,強制調用render(下個版本語義可能發生變化,不能保證一定會調用render)
  3. stateChange: setState && (!SCU || SCU()) 組件沒有實現SCU或組件的SCU返回true。

React的Component默認沒有實現SCU,所以如果組件沒有實現SCU,那麼setState一定會導致render,

React-redux的connect第四個參數options有個【pure=true】的配置,其為組件添加SCU,其對組件的props進行淺比較(默認是true),所以如果在reducer直接修改的原state的屬性,redux組件並不能保證會觸發render。

  1. propChange: 父組件render導致調用子組件的render或者調用ReactDOM.render && (!SCU || SCU),這都會觸發WillReceiveProps。其也會進行SCU檢查,與stateChange邏輯一致。

對於stateChange和propChange,並不能保證state和prop發生了改變,僅僅是觸發了該生命周期而已,prop是否發生改變需要用戶自己去進行檢查(涉及deepEqual),如果對象嵌套過深,則需考慮Immutable對象,減小deepEqual代價。

數據更新

render函數里渲染的數據可能分為四種(Demo6)。

  • this.state
  • this.props.mutable
  • this.props.immutable(如defaultValue)
  • this.instanceVariable

如果render函數里同時包含這四種數據,則需要注意數據更新時,及時觸發render進行更新,因為React並不會自動的為這些數據更新調用render函數。

常見問題

  • 如instanceVariable在構造函數里根據props進行初始化,當props發生變化時instanceVariable並未進行重新計算,導致渲染出錯。解決方式是1.在willReceiveProps里重新計算instanceVariable,2.不使用instanceVariable,而是在render里計算得到。3.模仿Vue那套,使用計算屬性(watch.js/Rx.js/mobx)。

非受控組件

暫時我們進行如下定義(Demo7):

  • 受控組件:組件的狀態維護在組件外部,組件響應props的變化,並提供向外派發命令的介面。
  • 非受控組件:組件狀態維護在組件內部,組件只根據config進行初始化,後續props的變化不會導致組件重渲染,並提供向外的onChange介面。
  • 混雜組件:組件的狀態既有一部分維護在外部,也有一部分維護在內部(或者說既可以從內部也可以從外部更新狀態),因此需要同步狀態。(混雜組件的好處在於既可以當做受控組件使用也可以當做非受控組件使用,使用方式十分靈活,antd的一些組件就是這樣做的,但是如果同時在外部和內部更新狀態則很容易出問題。)

受控組件和非受控組件都是單向數據流,受控組件數據流方向自外向內,非受控組件數據流方向自內向外,較為容易管理。而對於混雜組件,由於內部和外部都維護狀態,要處理好狀態同步,尤其在存在非同步的環境下(如Redux),很容易出現問題,所以不建議使用。

React官方建議大多數場景下應該使用受控組件,在某些場景下非受控組件也有其獨特優勢。

受控組件要求將組件狀態維護在組件外部,一方面對於一些較複雜的組件,可能涉及很多的狀態變數,放在外部維護會加重外部組件負擔和造成組件的介面比較複雜。另一方面一些第三方插件封裝為非受控組件也比受控組件更為容易。

topbuzz的編輯器內部業務就較為複雜(涉及圖片視頻的上傳,編輯器的編輯、存儲、發布、預覽、存草稿,草稿撤銷功能等),涉及很多的狀態,因此考慮將其封裝為非受控組件。

設計如下:編輯器只在首次mount的時候根據傳入的config初始化編輯器狀態,後續的編輯器編輯時的狀態均維護在組件內部,編輯器向外暴露notifyArticleChange介面。

這種方式,使用者僅僅需要傳入初始的值和onArticleChange回調即可使用編輯器,而不需要關心編輯器內部的數據存儲格式。

然而需求是不斷變動的,產品後來提了新需求,需要編輯器支持重新載入新內容功能,這也就要求我們的編輯器能夠重新根據新的config重新載入編輯器內容。

存在下面兩種方案(Demo8)

  1. 編輯器向外提供重新載入介面loadFromConfig,外部需要重新載入時通過調用editorInstance.loadFromConfig(newConfig)即可。
  2. 編輯器在willReceiveProps中響應config里article的變化,重新初始化state。該方案存在的問題是父組件的render會觸發編輯器的導致編輯器的render,在willReceiveProps里造成錯誤的重新初始化state(內部狀態被清除了)。根源在於此時編輯器的state即維護在內部,又受到外部影響,會造成內外狀態不一致。

非受控組件要解決的一個問題是如何防止父組件錯誤的更新子組件。(父組件的每次render都會觸發子組件的propChange),導致可能錯誤的更新(重構中碰到的一個問題就是編輯器內容經常被清空,就是因為父組件render,導致使用舊的config初始化state導致的)。

  1. 父組件傳入的props只負責做首次mount的初始化,因此render函數里的渲染數據應該不包含傳入的props或者只包含不變的props。

相關代碼react-communication

傳遞組件

重構nav時發現其中存在大量Editor相關業務代碼,nav和Editor通過全局事件進行通訊,nav中存儲Editor的實例。這樣存在如下幾個問題:

  1. nav和Editor通過事件通訊,nav監聽Editor的相關事件,這要求nav要在Editor事件觸發前已載入完畢,但是React並沒有提供控制不同組件載入順序的機制(React只能保證子組件先於父組件載入完畢,在React16中添加了AsyncComponent組件,可以進行非同步渲染,似乎可以解決這問題)。所以使用了監聽DomContentLoaded,這時能保證兩個組件都已載入完畢(對於非同步載入的組件這招好像行不通)。
  2. nav中保存Editor的instance,但是instance的屬性更新並不會觸發render,需要手動的forceUpdate觸發render,容易忘記。

因此考慮將業務收斂到編輯器,但發現nav中還涉及到很多編輯器的UI交互,對於數據和回調很容易放到編輯器里,但是UI的交互卻不容易收斂到編輯器,難道仍然要使用事件通信。

這時的問題變為A組件的UI渲染到B組件里(跨組件渲染)

React為解決跨組件渲染給了一個API

  • ReactDOM.unstable_renderSubtreeIntoContainer(parentElement,nextElement,container,callback)

其實這和ReactDOM.render的區別在於ReactDOM.render的parentElement是null。而parentElement只是提供了一個context。後續處理邏輯一致

if (parentComponent) {n var parentInst = ReactInstanceMap.get(parentComponent);n nextContext = parentInst._processChildContext(parentInst._context);n } else {n nextContext = emptyObject;n }n

但使用時發現A組件里的傳入的nextElement的props更新在B組件里進行重渲染,且A仍然需要依賴於B內的container節點已經存在。

解決方式是通過傳遞組件直接在編輯器生成Nav所需的UI組件,然後傳遞給Nav,編輯器直接負責UI組件的更新。


推薦閱讀:

手把手教你為 React 添加雙向數據綁定(一)
一年前端開發,學習永遠趕不上潮流,有一定的PHP基礎,現在動搖了,不知道該繼續前端,還是轉PHP?
前端開發,開發人員怎麼方便的自測IE各個版本?
天天演算法 | Easy | 10. 有效括弧:Valid Parentheses
我的第一個響應式頁面

TAG:React | 前端开发 |