앞에 쓰는 말
react-redux 는 접착제 같은 것으로, 깊이 이해할 필요가 없는 것처럼 보이지만, 실제로는 데이터 계층 (redux) 과 UI 계층 (react) 의 연결점으로서 그 구현 세부사항은 전체 성능에 결정적인 영향을 미칩니다. 컴포넌트 트리가 함부로 update 하는 비용은 reducer 트리를 몇 번 더 실행하는 비용보다 훨씬 높으므로 그 구현 세부사항을 이해할 필요가 있습니다
react-redux 를 자세히 이해하는 장점 중 하나는 성능에 대한 기본적인 인식을 가질 수 있다는 것입니다. 다음 문제를 고려해 보세요:
dispatch({type: 'UPDATE_MY_DATA', payload: myData})
컴포넌트 트리 어딘가의 이 코드가 가져오는 성능 영향은 무엇일까요? 몇 가지 하위 문제:
-
1.어떤 reducer 가 재계산되었나요?
-
2.발생한 뷰 업데이트는 어떤 컴포넌트에서 시작되나요?
-
3.어떤 컴포넌트의 render 가 호출되었나요?
-
4.모든 리프 컴포넌트가 diff 의 영향을 받았나요? 왜요?
이러한 문제들에 정확히 답할 수 없다면 성능에 대해 확신이 없을 것입니다
一.역할
먼저, redux 는 단순한 데이터 계층이고 react 는 단순한 UI 계층이며, 둘 사이에는 연관이 없음을 명확히 합니다
왼손과 오른손으로 각각 redux 와 react 를 들고 있다면, 실제 상황은 이렇습니다:
-
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 후 react view 를 update 하는 책임
추측컨대 구현은 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 를 전개하여 속성을 타겟 객체에 병합
기본 파라미터는 es6 기능으로, 말할 것이 없습니다. 객체 구조분해는 Stage 3 proposal 이며, ...others는 그 기본 사용법입니다. 전개 연산자는 객체를 전개하여 타겟 객체에 병합합니다. 복잡하지 않습니다
흥미로운 것은 여기서 객체 구조분해와 전개 연산자를配合하여 사용하여 파라미터에 대해팩킹-복원이 필요한 시나리오를 구현했다는 것입니다. 이 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 인스턴스는dispatch라는 prop 을 얻을 수 있습니다. 어디에서 몰래 달아놓은 것일까요?
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.주석은 여전히 충분히 상세합니다.
아직 댓글이 없습니다