寫在前面
react-redux做爲膠水同樣的東西,彷佛沒有深刻了解的必要,但實際上,做爲數據層(redux)與UI層(react)的鏈接處,其實現細節對總體性能有着決定性的影響。組件樹胡亂update的成本,要比多跑幾遍reducer樹的成本高得多,因此有必要了解其實現細節react
仔細瞭解react-redux的好處之一是能夠對性能有基本的認識,考慮一個問題:git
dispatch({type: 'UPDATE_MY_DATA', payload: myData})
組件樹中某個角落的這行代碼,帶來的性能影響是什麼?幾個子問題:es6
1.致使哪些reducer被從新計算了?github
2.引起的視圖更新從哪一個組件開始?redux
3.哪些組件的render被調用了?緩存
4.每一個葉子組件都被diff波及了嗎?爲何?性能優化
若是沒法準確回答這幾個問題,對性能確定是內心沒底的數據結構
一.做用
首先,明確redux只是一個數據層,而react只是一個UI層,兩者之間是沒有聯繫的app
若是左右手分別拿着redux和react,那麼實際狀況應該是這樣的:ide
redux把數據結構(state)及各字段的計算方式(reducer)都定好了
react根據視圖描述(Component)把初始頁面渲染出來
多是這個樣子:
redux | react myUniversalState | myGreatUI human | noOneIsHere soldier | arm | littleGirl | toy | ape | noOneIsHere hoho | tree | someTrees mountain | someMountains snow | flyingSnow
左邊redux裏什麼都有,可是react不知道,只顯示了默認元素(沒有沒有數據),有一些組件局部state和零散的props傳遞,頁面就像一幀靜態圖,組件樹看起來只是由一些管道鏈接起來的大架子
如今咱們考慮把react-redux加進來,那麼就會變成這樣子:
react-redux redux -+- react myUniversalState | myGreatUI HumanContainer human -+- humans soldier | soldiers ArmContainer arm -+- arm littleGirl | littleGirl toy | toy ApeContainer ape -+- apes hoho | hoho SceneContainer tree -+- Scene mountain | someTrees snow | someMountains flyingSnow
注意,Arm交互比較複雜,不適合由上層(HumanContainer)控制,因此出現了嵌套Container
Container把redux手裏的state交給react,這樣初始數據就有了,那麼若是要更新視圖呢?
Arm.dispatch({type: 'FIRST_BLOOD', payload: warData})
有人打響了第一槍,致使soldier掛了一個(state change),那麼這些部分要發生變化:
react-redux redux -+- react myNewUniversalState | myUpdatedGreatUI HumanContainer human -+- humans soldier | soldiers | diedSoldier ArmContainer arm -+- arm | inactiveArm
頁面上出現一個掛掉的soldier和一支掉地上的arm(update view),其它部分(ape, scene)一切安好
上面描述的就是react-redux的做用:
把state從redux傳遞到react
並負責在redux state change後update react view
那麼猜也知道,實現分爲3部分:
給管道鏈接起來的大架子添上一個個小水源(經過Container把state做爲props注入下方view)
讓小水源冒水(監聽state change,經過Container的setState來更新下方view)
不小水源不要亂冒(內置性能優化,對比緩存的state, props看有沒有必要更新)
二.關鍵實現
源碼關鍵部分以下:
// from: src/components/connectAdvanced/Connect.onStateChange onStateChange() { // state change時從新計算props this.selector.run(this.props) // 當前組件不用更新的話,通知下方container檢查更新 // 要更新的話,setState空對象強制更新,延後通知到didUpdate if (!this.selector.shouldComponentUpdate) { this.notifyNestedSubs() } else { this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate // 通知Container下方的view更新 //!!! 這裏是把redux與react鏈接起來的關鍵 this.setState(dummyState) } }
最重要的那個setState就在這裏,dispatch action後視圖更新的祕密是這樣:
1.dispatch action 2.redux計算reducer獲得newState 3.redux觸發state change(調用以前經過store.subscribe註冊的state變化監聽器) 4.react-redux頂層Container的onStateChange觸發 1.從新計算props 2.比較新值和緩存值,看props變了沒,要不要更新 3.要的話經過setState({})強制react更新 4.通知下方的subscription,觸發下方關注state change的Container的onStateChange,檢查是否須要更新view
第3步裏,react-redux向redux註冊store change監聽的動做發生在connect()(myComponent)時,事實上react-redux只對頂層Container直接監聽了redux的state change,下層Container都是內部傳遞通知的,以下:
// from: src/utils/Subscription/Subscription.trySubscribe trySubscribe() { if (!this.unsubscribe) { // 沒有父級觀察者的話,直接監聽store change // 有的話,添到父級下面,由父級傳遞變化 this.unsubscribe = this.parentSub ? this.parentSub.addNestedSub(this.onStateChange) : this.store.subscribe(this.onStateChange) } }
這裏不直接監聽redux的state change,而非要本身維護Container的state change listener,是爲了實現次序可控,例如上面提到的:
// 要更新的話,延後通知到didUpdate this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
這樣保證了listener觸發順序是按照組件樹層級順序的,先通知大子樹更新,大子樹更新完畢後,再通知小子樹更新
更新的整個過程就是這樣,至於「經過Container把state做爲props注入下方view」這一步,沒什麼好說的,以下:
// from: src/components/connectAdvanced/Connect.render render() { return createElement(WrappedComponent, this.addExtraProps(selector.props)) }
根據WrappedComponent須要的state字段,造一份props,經過React.createElement注入進去。ContainerInstance.setState({})時,這個render函數被從新調用,新的props被注入到view,view will receive props…視圖更新就真正開始了
三.技巧
讓純函數擁有狀態
function makeSelectorStateful(sourceSelector, store) { // wrap the selector in an object that tracks its results between runs. const selector = { run: function runComponentSelector(props) { try { const nextProps = sourceSelector(store.getState(), props) if (nextProps !== selector.props || selector.error) { selector.shouldComponentUpdate = true selector.props = nextProps selector.error = null } } catch (error) { selector.shouldComponentUpdate = true selector.error = error } } } return selector }
把純函數用對象包起來,就能夠有局部狀態了,做用和new Class Instance相似。這樣就把純的部分與不純的部分分離開了,純的依然純,不純的在外面,class不如這個乾淨
默認參數與對象解構
function connectAdvanced( selectorFactory, // options object: { getDisplayName = name => `ConnectAdvanced(${name})`, methodName = 'connectAdvanced', renderCountProp = undefined, shouldHandleStateChanges = true, storeKey = 'store', withRef = false, // additional options are passed through to the selectorFactory ...connectOptions } = {} ) { const selectorFactoryOptions = { // 展開 還原回去 ...connectOptions, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, withRef, displayName, wrappedComponentName, WrappedComponent } }
能夠簡化成這樣:
function f({a = 'a', b = 'b', ...others} = {}) { console.log(a, b, others); const newOpts = { ...others, a, b, s: 's' }; console.log(newOpts); } // test f({a: 1, c: 2, f: 0}); // 輸出 // 1 "b" {c: 2, f: 0} // {c: 2, f: 0, a: 1, b: "b", s: "s"}
這裏用到3個es6+小技巧:
默認參數。防止解構時右邊undefined報錯
對象解構。把剩餘屬性都包進others對象裏
展開運算符。把others展開,屬性merge到目標對象上
默認參數是es6特性,沒什麼好說的。對象解構是Stage 3 proposal,...others是其基本用法。展開運算符把對象展開,merge到目標對象上,也不復雜
比較有意思的是這裏把對象解構和展開運算符配合使用,實現了這種須要對參數作打包-還原的場景,若是不用這2個特性,可能須要這樣作:
function connectAdvanced( selectorFactory, connectOpts, otherOpts ) { const selectorFactoryOptions = extend({}, otherOpts, getDisplayName, methodName, renderCountProp, shouldHandleStateChanges, storeKey, withRef, displayName, wrappedComponentName, WrappedComponent ) }
須要清楚地區分connectOpts和otherOpts,實現上會麻煩一些,組合運用這些技巧的話,代碼至關簡練
另外還有1個es6+小技巧:
addExtraProps(props) { //! 技巧 淺拷貝保證最少知識 //! 淺拷貝props,不把別人不須要的東西傳遞出去,不然影響GC const withExtras = { ...props } }
多一份引用就多一分內存泄漏的風險,不須要的不該該給(最少知識)
參數模式匹配
function match(arg, factories, name) { for (let i = factories.length - 1; i >= 0; i--) { const result = factories[i](arg) if (result) return result } return (dispatch, options) => { throw new Error(`Invalid value of type ${typeof arg} for ${name} argument when connecting component ${options.wrappedComponentName}.`) } }
其中factories是這樣:
// mapDispatchToProps [ whenMapDispatchToPropsIsFunction, whenMapDispatchToPropsIsMissing, whenMapDispatchToPropsIsObject ] // mapStateToProps [ whenMapStateToPropsIsFunction, whenMapStateToPropsIsMissing ]
針對參數的各類狀況創建一系列case函數,而後讓參數依次流經全部case,匹配任意一個就返回其結果,都不匹配就進入錯誤case
相似於switch-case,用來對參數作模式匹配,這樣各類case都被分解出去了,各自職責明確(各case函數的命名很是準確)
懶參數
function wrapMapToPropsFunc() { // 猜完當即算一遍props let props = proxy(stateOrDispatch, ownProps) // mapToProps支持返回function,再猜一次 if (typeof props === 'function') { proxy.mapToProps = props proxy.dependsOnOwnProps = getDependsOnOwnProps(props) props = proxy(stateOrDispatch, ownProps) } }
其中,懶參數是指:
// 把返回值做爲參數,再算一遍props if (typeof props === 'function') { proxy.mapToProps = props proxy.dependsOnOwnProps = getDependsOnOwnProps(props) props = proxy(stateOrDispatch, ownProps) }
這樣實現和react-redux面臨的場景有關,支持返回function主要是爲了支持組件實例級(默認是組件級)的細粒度mapToProps控制。這樣就能針對不一樣組件實例,給不一樣的mapToProps,支持進一步提高性能
從實現上來看,至關於把實際參數延後了,支持傳入一個參數工廠做爲參數,第一次把外部環境傳遞給工廠,工廠再根據環境造出實際參數。添了工廠這個環節,就把控制粒度細化了一層(組件級的細化到了組件實例級,外部環境即組件實例信息)
P.S.關於懶參數的相關討論見https://github.com/reactjs/react-redux/pull/279
四.疑問
1.默認的props.dispatch哪裏來的?
connect()(MyComponent)
不給connect傳任何參數,MyComponent實例也能拿到一個prop叫dispatch,是在哪裏偷偷掛上的?
function whenMapDispatchToPropsIsMissing(mapDispatchToProps) { return (!mapDispatchToProps) // 就是這裏掛上去的,沒傳mapDispatchToProps的話,默認把dispatch掛到props上 ? wrapMapToPropsConstant(dispatch => ({ dispatch })) : undefined }
默認內置了一個mapDispatchToProps = dispatch => ({ dispatch }),因此組件props身上有dispatch,若是指定了mapDispatchToProps,就不給掛了
2.多級Container會不會面臨性能問題?
考慮這種場景:
App HomeContainer HomePage HomePageHeader UserContainer UserPanel LoginContainer LoginButton
出現了嵌套的container,那麼在HomeContainer關注的state發生變化時,會不會走不少遍視圖更新?好比:
HomeContainer update-didUpdate UserContainer update-didUpdate LoginContainer update-didUpdate
若是是這樣,輕輕一發dispatch,致使3個子樹更新,感受性能要炸了
實際上不是這樣。對於多級Container,走兩遍的狀況確實存在,只是這裏的走兩遍不是指視圖更新,而是說state change通知
上層Container在didUpdate後會通知下方Container檢查更新,可能會在小子樹再走一遍。但在大子樹更新的過程當中,走到下方Container時,小子樹在這個時機就開始更新了,大子樹didUpdate後的通知只會讓下方Container空走一遍檢查,不會有實際更新
檢查的具體成本是分別對state和props作===比較和淺層引用比較(也是先===比較),發現沒變就結束了,因此每一個下層Container的性能成本是兩個===比較,沒關係。也就是說,不用擔憂使用嵌套Container帶來的性能開銷
五.源碼分析
Github地址:https://github.com/ayqy/react-redux-5.0.6
P.S.註釋依然足夠詳盡。