react-redux 雜談- 設計結構變遷

blank

react-redux 雜談- 設計結構變遷

本文默认读者已经对 react + redux 技术栈有过一定了解。

redux相信大多前端同學都有聽過,官方是這麼描述的: A predictable state container for JavaScript apps 。如果恰好又做過react相關的開發,那麼對react-redux應該也不陌生,因為它是Official React bindings for Redux ,可以看出react-redux的實現強依賴著react。

隨著react各種新特性的增加, react-redux也隨之進行著升級調整。本文是在閱讀源碼的過程中,對[email protected] 以及之後版本的一些變遷做的零碎記錄。

結構設計

[email protected]

[email protected] 的版本中,react-redux 的結構大致是這樣的(如果不考慮多個store 等情況)

blank

虛線是通過connect方法生成的“容器組件(Connect) ”,而實線則是我們的業務組件。

/* hello-world.js */importReactfrom'react';import{connect}from'react-redux';@connect(mapStateToProps,mapDispatchToProps,...otherConnectOptions)exportdefaultclassHelloWorldextendsReact.Component{render(){...}}/* index.js */importReactfrom'react';importHelloWorldfrom'./hello-world';exportdefaultfunctionApp(){return(<><HelloWorld/></>);}

index.js 中import 的HelloWorld 組件是一個被Connect 包裹的HelloWorld。

Connect組件擁有onStateChange方法,當實例化的時候,會從context上找上一級的subscription (parentSub),並把onStateChange放在parentSub的listeners: (() => void)[]中,這樣上一級(當前組件的父組件可能並沒有被Connect 包裹,只是一個普通的組件,就像圖1中最右側畫的那樣)Connect 組件就能夠在必要的時候去執行回調隊列,觸發下級組件的更新。

classConnect{initSubscription(){...//獲取上一級subscriptionconstparentSub=(this.propsMode?this.props:this.context)[subscriptionKey]//生成當前組件的subscription,並且將onStateChange放入上級subscription的listeners中this.subscription=newSubscription(this.store,parentSub,this.onStateChange.bind(this))...}shouldComponentUpdate(){returnthis.selector.shouldComponentUpdate}onStateChange(){//檢查是否需要觸發renderthis.selector.run(this.props)//如果當前組件訂閱的state中的數據沒有變化&&接收到的props也沒有變化//當前Connect不需要render//執行回調隊列通知下級Connect組件去檢查是否需要renderif(!this.selector.shouldComponentUpdate){this.notifyNestedSubs()}else{this.componentDidUpdate=this.notifyNestedSubsOnComponentDidUpdate// const dummyState = {}this.setState(dummyState)}}}

同時,當前Connect 組件會生成自己的subscription,然後重寫context

getChildContext(){constsubscription=this.propsMode?null:this.subscriptionreturn{[subscriptionKey]:subscription||this.context[subscriptionKey]}}

這樣,Connect 的下級組件從contex 上拿到的就是Connect 生成的subscription,同時下級組件會把自己的onStateChange 放入它的subscription listeners 中,在有需要時觸發回調隊列,“跨層級”的通知組件render 。

[email protected]

在5.x 版本中,如果垂直多個層級的組件都使用connect 訂閱了全局state 的變化

@connect(...)classAextendsReact.Component{}@connect(...)classBextendsReact.Component{render(){...<A/>...}}@connect(...)classCextendsReact.Component{render(){...<B/>...}}

當state更新時,“消息”是C --> B --> A一層一層往下傳遞的,而不是統一維護在store的listeners中。這種非集中式的結構看起來有點複雜。有一天react新增加了一個Contex API的東西

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

react 原生支持數據的跨組件消費和rerender,看起來很棒,而且用Contex API 的話似乎能夠讓react-redux 結構簡化不少,性能或許也會更好一些

Ironically, one of the reasons what we're moving to createContext is because I had hoped that switching to a single subscription and passing the state down via context, instead of N subscriptions, would be faster.

然後經過一番操作,react-redux 的大版本號從5 升級到了6,整個層級結構大概變成了這樣

blank

Provider變成了這樣

classProviderextendsComponent{constructor(props){super(props)const{store}=propsthis.state={storeState:store.getState(),store}}componentDidMount(){this._isMounted=truethis.subscribe()}...subscribe(){const{store}=this.props//放入store的回調隊列中//當store.state變化,重寫Provide value值,觸發Consumer rerenderthis.unsubscribe=store.subscribe(()=>{constnewStoreState=store.getState()if(!this._isMounted){return}this.setState(providerState=>{// If the value is the same, skip the unnecessary state update.if(providerState.storeState===newStoreState){returnnull}return{storeState:newStoreState}})})constpostMountStoreState=store.getState()if(postMountStoreState!==this.state.storeState){this.setState({storeState:postMountStoreState})}}render(){constContext=this.props.context||ReactReduxContextreturn(<Context.Providervalue={this.state}>{this.props.children}</Context.Provider>)}}

Connect的render方法變成了這樣。

render(){constContextToUse=this.props.context||Contextreturn(<ContextToUse.Consumer>{this.renderWrappedComponent}</ContextToUse.Consumer>)}

這樣做讓整體結構變得十分簡單,每次state 變化只需要修改Provider 的value 值,react 會逐個找到消費數據的Consumer 進行更新。

但是......很多開發者升級後發現,6.x 版本性能似乎比之前的差了好多

導致性能問題的原因在Performance downgrade after update to v.6.0.0這個issue中有比較多的討論, markerikson是這麼說的

Specifically, in v5, connected components re-ran mapState immediately in the subscribe callbacks, and only called setState() once they knew they needed to re-render. In v6, every store update calls setState() at the root of the component tree in <Provider> , and forces React to walk the component tree to find the consumers before they can even run mapState . That's a very meaningful difference.

[email protected]

[email protected] 重新使用了多個subscription 逐層通知的結構,和5.x 基本一致,並且使用React Hooks 對項目進行了重構,增加了對hooks 的支持。在API 的設計以及代碼實現方面也有很多的思考以及argue (譬如關於useDispatch 和useAction ),後面有空會再寫一下。

性能對比

react-redux-benchmarks項目對三個版本的的react-redux進行了性能測試

Results for benchmark deeptree: +---------------------------------------- ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +--------------+---------+----------- ---+ ¦ 5.1.1 ¦ 11.65 ¦ 155.3, 0.1 ¦ +--------------+---------+--------- -----+ ¦ 6.0.0 ¦ 6.88 ¦ 121.8, 8.0 ¦ +--------------+---------+------- -------+ ¦ 7.0.0-beta.0 ¦ 19.39 ¦ 133.6, 1.3 ¦ +-------------------------- -------------- Results for benchmark deeptree-nested: +---------------------------- ------------ ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +--------------+----- ----+--------------+ ¦ 5.1.1 ¦ 41.67 ¦ 179.1, 1.1 ¦ +--------------+--- ------+--------------+ ¦ 6.0.0 ¦ 7.31 ¦ 182.5, 10.8 ¦ +--------------+- --------+--------------+ ¦ 7.0.0-beta.0 ¦ 42.33 ¦ 204.8, 0.7 ¦ +---------- ------------------------------ Results for benchmark forms: +-------------- -------------------------- ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +------ --------+---------+--------------+ ¦ 5.1.1 ¦ 36.87 ¦ 1434.0, 0.4 ¦ +---- ---- ------+---------+--------------+ ¦ 6.0.0 ¦ 24.91 ¦ 1424.9, 3.3 ¦ +------ --------+---------+--------------+ ¦ 7.0.0-beta.0 ¦ 34.51 ¦ 1446.2, 0.4 ¦ + ---------------------------------------- Results for benchmark stockticker: +---- ------------------------------------ ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +--------------+---------+--------------+ ¦ 5.1.1 ¦ 27.58 ¦ 292.5, 0.7 ¦ +--------------+---------+--------------+ ¦ 6.0.0 ¦ 11.38 ¦ 265.1, 9.1 ¦ +--------------+---------+--------------+ ¦ 7.0 .0-beta.0 ¦ 28.61 ¦ 318.0, 0.4 ¦+---------------------------------------- Results for benchmark tree-view: +- --------------------------------------- ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +--------------+---------+--------------+ ¦ 5.1. 1 ¦ 41.49 ¦ 747.7, 0.3 ¦ +--------------+---------+--------------+ ¦ 6.0.0 ¦ 26.83 ¦ 682.8, 24.1 ¦ +--------------+---------+-------------- + ¦ 7.0.0-beta.0 ¦ 40.51 ¦ 780.7, 0.3 ¦ +--------------------------------- ------- Results for benchmark twitter-lite: +----------------------------------- ----- ¦ Version ¦ Avg FPS ¦ Render ¦ ¦ ¦ ¦ (Mount, Avg) ¦ +--------------+---------+-- ------------+ ¦ 5.1.1 ¦ 46.36 ¦ 3.4, 0.5 ¦ +--------------+---------+ --------------+ ¦ 6.0.0 ¦ 26.72 ¦ 4.0, 5.3 ¦ +--------------+-------- -+--------------+ ¦ 7.0.0-beta.0 ¦ 46.34 ¦ 3.7, 0.3 ¦ +----------------- -----------------------

可以看到,5.x和7.x性能相當,6.x性能較差,而6.x的正式npm包只有[email protected][email protected]兩個版本,之後就升級到7 了。

寫在最後

前段時間部門招人,看到有些簡歷上寫熟悉react + redux 技術棧,就問了一個問題:

classAextendsReact.Component{render(){<><B/><C/><D/></>}}@connect(state=>({num:state.num,}))classBextendsReact.Component{render(){returnthis.props.num;}}

當state 中的num 變化時,為什麼A 沒有rerender,作為子組件的B 卻能夠rerender

  • A 沒有傳遞任何props 值
  • B 中的state 沒有變化
  • B 也沒有消費任何Context 上面的數據

很多同學都知道用了connect 後組件就會自動rerender,但是卻講不太清楚具體的原因,以及多個subscription 的層級結構,希望以上文字能夠表述清楚此中原理。

另外,[email protected] 使用Context API 在比較大型應用上表現出的性能問題,也希望大家做類似操作時能夠注意。

閱讀優秀的源碼,並且了解作者們在源碼背後的思考,能夠幫助我們在寫(ban)碼(zhuan)時想的更全面、做的更優雅。

What do you think?

Written by marketer

blank

Vapperjs – 一個基於Vue 的SSR 框架

blank

John Z. Sonmez 的十步學習法體系