EDITORIALの
react-reduxglueは、同じこととして理解していないようですが、実際には、データレイヤー(redux)とUIレイヤー(react)の接続、およびその実装の詳細が全体的なパフォーマンスに決定的な影響を及ぼします。コンポーネントツリーのランダム更新のコストは、レデューサーツリーを数回実行するコストよりもはるかに高いため、その実装の詳細を理解する必要があります。
react-reduxを注意深く理解することの利点の1つは、パフォーマンスの基本を理解できることです。質問を検討してください。
dispatch({type: 'UPDATE_MY_DATA'、payload:myData})
コンポーネントツリーの隅にあるこのコード行のパフォーマンスへの影響は何ですか?いくつかのサブ問題:
1.どのレデューサーが再計算されましたか?
2.トリガーされたビューの更新はどのコンポーネントから開始されますか?
3.どのコンポーネントのレンダリングが呼び出されますか?
4.すべての葉のコンポーネントはdiffの影響を受けていますか?どうして?
これらの質問に正確に答えられない場合は、パフォーマンスについてまったくわかりません。
1.機能
まず第一に、reduxはデータレイヤーのみであり、reactはUIレイヤーのみであり、2つの間に接続がないことは明らかです。
左手と右手がそれぞれreduxを保持して反応する場合、実際の状況は次のようになります。
Reduxは、各フィールドのデータ構造(状態)と計算方法(リデューサー)を設定しました。
Reactは、ビューの説明(コンポーネント)に従って最初のページをレンダリングします
次のようになります。
redux | react
myUniversalState | myGreatUI
human | noOneIsHere
soldier |
arm |
littleGirl |
toy |
ape | noOneIsHere
hoho |
tree | someTrees
mountain | someMountains
snow | flyingSnow
左側にすべてが冗長になっていますが、Reactは認識していません。デフォルトの要素のみが表示され(データなし)、コンポーネントの部分的な状態と散在する小道具がいくつかあり、ページは静止画のようで、コンポーネントツリーはパイプのように見えます。接続された大きな棚
ここで、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)による制御には適していないため、ネストされたコンテナーがあることに注意してください。
コンテナは状態をreduxで渡して反応し、初期データが利用できるようにします。ビューを更新する必要がある場合はどうなりますか?
Arm.dispatch({type: 'FIRST_BLOOD'、payload:warData})
誰かが最初のショットを発射し、兵士に(状態の変化)を掛けさせた後、これらの部分を変更する必要があります。
react-redux
redux -+- react
myNewUniversalState | myUpdatedGreatUI
HumanContainer
human -+- humans
soldier | soldiers
| diedSoldier
ArmContainer
arm -+- arm
| inactiveArm
死んだ兵士と落とされた腕(更新ビュー)がページに表示され、他のすべては問題ありません(猿、シーン)
上記の説明は、react-reduxの役割です。
reduxから状態を渡して反応する
また、redux状態の変更後にreactビューを更新する責任があります
それからあなたはそれを推測しました、実装は3つの部分に分かれています:
パイプラインで接続された大きな棚に小さな水源を追加します(コンテナを通して下のビューに小道具として状態を注入します)
小さな水源に水を与えます(状態の変化を監視し、コンテナのsetStateを介して以下のビューを更新します)
小さな水源はなく、急いではいけません(組み込みのパフォーマンス最適化、キャッシュされた状態と小道具を比較して、更新が必要かどうかを確認します)
2.
ソースコードの重要な部分を実現するための鍵は次のとおりです。
// 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はここにあります。ディスパッチアクション後のビュー更新の秘密は次のとおりです。
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では、connect()(myComponent)のときにreact-reduxがストア変更監視をreduxに登録するアクションが発生します。実際、react-reduxは、最上位コンテナのreduxの状態変更のみを直接監視し、下位レベルのコンテナはすべて内部で通知を送信します。 、 次のように:
// 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の状態変更を直接監視しません。
// 要更新的话,延后通知到didUpdate
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
これにより、リスナーのトリガーシーケンスがコンポーネントツリー階層の順序になります。最初に大きなサブツリーに通知され、大きなサブツリーが更新された後、小さなサブツリーに更新が通知されます。
全体の更新プロセスは次のようになります。「コンテナを介して下のビューに小道具として状態を挿入する」ステップについては、次のように言うことは何もありません。
// from: src/components/connectAdvanced/Connect.render
render() {
return createElement(WrappedComponent, this.addExtraProps(selector.props))
}
WrappedComponentに必要な状態フィールドに基づいて小道具を作成し、React.createElementを介してそれに挿入します。ContainerInstance.setState({})の場合、render関数が再度呼び出され、新しい小道具がビューに挿入され、ビューは小道具を受け取ります...ビューの更新が実際に開始されます
3.純粋な関数を作成する手法に
は状態があります
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
}
純粋な関数をオブジェクトでラップして、新しいクラスインスタンスと同様のローカル状態にします。このようにして、純粋な部分が不純な部分から分離されます。純粋な部分はまだ純粋で、不純な部分は外側にあります。クラスはこれほどきれいではありません。
デフォルトのパラメータとオブジェクトの分解
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 +のヒントがあります:
デフォルトのパラメータ。解体中の右側の未定義エラーを防止します
オブジェクトの分解。残りの属性を他のオブジェクトにラップします
演算子を展開します。他を展開し、属性をターゲットオブジェクトにマージします
デフォルトのパラメータはes6機能であり、言うことは何もありません。オブジェクトの分解はステージ3の提案であり、その他はその基本的な使用法です。展開演算子はオブジェクトを展開し、それをターゲットオブジェクトにマージします。これは複雑ではありません。
さらに興味深いのは、ここではオブジェクトの分解演算子と展開演算子の組み合わせを使用して、パラメーターをパッケージ化して復元する必要があるこのシナリオを実現していることです。これら2つの機能を使用しない場合は、次のようにする必要があります。
function connectAdvanced(
selectorFactory,
connectOpts,
otherOpts
) {
const selectorFactoryOptions = extend({},
otherOpts,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
withRef,
displayName,
wrappedComponentName,
WrappedComponent
)
}
connectOptsとotherOptsを明確に区別する必要があります。実装が面倒になります。これらの手法を組み合わせると、コードは非常に簡潔になります。
es6 +のヒントもあります。
addExtraProps(props) {
//! 技巧 浅拷贝保证最少知识
//! 浅拷贝props,不把别人不需要的东西传递出去,否则影响GC
const withExtras = { ...props }
}
もう1つの参照は、メモリリークのもう1つのリスクを意味し、不要なものに与えられるべきではありません(最小限の知識)
パラメータパターンマッチング
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}.`)
}
}
工場は次のようなものです。
// mapDispatchToProps
[
whenMapDispatchToPropsIsFunction,
whenMapDispatchToPropsIsMissing,
whenMapDispatchToPropsIsObject
]
// mapStateToProps
[
whenMapStateToPropsIsFunction,
whenMapStateToPropsIsMissing
]
パラメータのさまざまな条件に対して一連のケース関数を作成し、パラメータをすべてのケースに順番に流し、いずれかが一致する場合は結果を返し、一致しない場合はエラーケースを入力します
スイッチケースと同様に、パラメータのパターンマッチングを実行するために使用されるため、さまざまなケースが分解され、それぞれの責任が明確になります(各ケース関数の名前は非常に正確です)。
怠惰なパラメータ
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が直面するシナリオに関連しています。return関数は、主にコンポーネントインスタンスレベル(デフォルトはコンポーネントレベル)で詳細なmapToPropsコントロールをサポートするためのものです。このようにして、さまざまなmapToPropsをさまざまなコンポーネントインスタンスに与えて、パフォーマンスをさらに向上させることができます。
実装の観点からは、実際のパラメータを遅らせることと同等であり、パラメータファクトリをパラメータとして渡すことをサポートします。外部環境が初めてファクトリに渡され、ファクトリは環境に応じて実際のパラメータを作成します。ファクトリリンクの追加により、コントロールの粒度が改善されます(コンポーネントレベルはコンポーネントインスタンスレベルに改善され、外部環境はコンポーネントインスタンス情報です)
PSレイジーパラメータに関する関連する議論については、https://github.com/reactjs/react-redux/pull/279を参照してください。
4.質問
1.デフォルトのprops.dispatchはどこから来たのですか?
connect()(MyComponent)
接続するパラメータを渡さなくても、MyComponentインスタンスはdispatchと呼ばれる小道具を取得できます。どこで密かにハングしましたか?
function whenMapDispatchToPropsIsMissing(mapDispatchToProps) {
return (!mapDispatchToProps)
// 就是这里挂上去的,没传mapDispatchToProps的话,默认把dispatch挂到props上
? wrapMapToPropsConstant(dispatch => ({ dispatch }))
: undefined
}
mapDispatchToProps = dispatch =>({dispatch})がデフォルトで組み込まれているため、コンポーネントの小道具にディスパッチがあります。mapDispatchToPropsが指定されている場合、ハングしません。
2.マルチレベルコンテナはパフォーマンスの問題に直面しますか?
このシナリオを検討してください。
App
HomeContainer
HomePage
HomePageHeader
UserContainer
UserPanel
LoginContainer
LoginButton
ネストされたコンテナが表示された場合、HomeContainerに関連する状態が変化すると、ビューの更新が何度も繰り返されますか?といった:
HomeContainer update-didUpdate
UserContainer update-didUpdate
LoginContainer update-didUpdate
この場合、軽いディスパッチにより3つのサブツリーが更新され、パフォーマンスが爆発しそうな気がします。
これはそうではありません。マルチレベルコンテナの場合、2回実行する状況は存在しますが、ここで2回実行することは、ビューの更新ではなく、状態変更の通知を指します。
上のコンテナは、didUpdateの後に更新を確認するように下のコンテナに通知し、小さなサブツリーを再度通過する場合があります。ただし、大きなサブツリーを更新する過程で、下位のコンテナに移動すると、この時点で小さなサブツリーの更新が開始されます。大きなサブツリーのdidUpdate後の通知では、実際の更新なしで下位のコンテナのみがチェックを通過します。
検査の具体的なコストは、状態と小道具を===比較と浅い参照比較(また===最初の比較)で比較することであり、変更がなければ終了するため、各下位レベルのコンテナのパフォーマンスコストは2です===比較、それは問題ではありません。つまり、ネストされたコンテナを使用することによるパフォーマンスのオーバーヘッドについて心配する必要はありません。
5.ソースコード分析
Githubアドレス:https://github.com/ayqy/react-redux-5.0.6
PSのコメントはまだ十分に詳細です。