react-redux 深入探究

之前的文章我们讲解了数据流的作用,还有 redux 的源码。现在思考,如何将 redux 应用到 react 上面呢?
首先我们想到 store 可能被多个组件依赖和影响,为此我们应当借助 context 来存储 store 数据。将 store 存储在根元素上,通过 context 进行数据的传递。为了实现这一步,react-redux 提供了 <Provider>

Provider

Provider 简单代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
class Provider extends Component {
constructor(props) {
super(props)
// 获取store
const { store } = props
// 初始化state, storeState为初始的redux state
this.state = {
storeState: store.getState(),
// 保存init store
store
}
}
componentDidMount() {
// 订阅 store 变更
this.subscribe()
}
componentWillUnmount() {
// 取消订阅
this.unsubscribe();
}
componentDidUpdate() {
// 如果更新的过程中store改变引用
if (this.props.store !== prevProps.store) {
// 如果存在监听则取消
if (this.unsubscribe) this.unsubscribe()
// 更新storeState
this.subscribe()
}
}
subscribe() {
const { store } = this.props
// 监听subscribe
this.unsubscribe = store.subscribe(() => {
// 获取最新的state赋值给newStoreState
const newStoreState = store.getState()
// 不在本次生命周期中return
if (!this._isMounted) {
return
}
this.setState(providerState => {
// If the value is the same, skip the unnecessary state update.
// 如果state是相同的引用, 直接跳过state的更新
if (providerState.storeState === newStoreState) {
return null
}
// 更新当前storeState
return { storeState: newStoreState }
})
})
}
render() {
// ReactReduxContext = React.createContext(null);
const Context = this.props.context || ReactReduxContext
return (
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
)
}
}

我们可以看到,在 Provider 准备完毕后,将 state.store 作为 context 的值。并且监听 store 的变化,如果有变化则调用 setState 来更新 context。这里用到的 context 是 React 16+ 之后的新版本写法。之后会对其进行介绍。
现在我们子组件想要使用 store 数据的就可以包装在 <Context.Customer> 里面即可。当然这中间少不了一些优化和功能的增强,这些方法都被封撞在了 connect 中。

connect

首先看下 connect 的用法:

1
connect([mapStateToProps],[mapDispatchToProps],[mergeProps],[options])(<Component / >)

connect 简化版代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export function createConnect({
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {}) {
// connect方法
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true, // 是否就行浅比较的配置
strictEqual, // 判断object引用, strictEqual(a, b)=> a === b
shallowEqual, // 浅比较
...extraOptions // 其他配置项
} = {}
) {
// 下面,分别初始化了各自的参数 mapStateToProps,mapDispatchToProps,mergeProps,并且包装了一些内默认参数和方法
const initMapStateToProps = match(...args)
const initMapDispatchToProps = match(...args)
const initMergeProps = match(...args)
// 核心部分,根据传入参数,返回包装好的高阶组件
return connectAdvanced(selectorFactory, {
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
...extraOptions
})
}
}
// 直接执行createConnect方法返回connect
export default createConnect()

先说明一下该函数的参数:

  • connectHOC: 一个重要组件,用于执行已确定的逻辑,渲染最终组件,后面会详细说。
  • mapStateToPropsFactories: 对 mapStateToProps 这个传入的参数的类型选择一个合适的方法。
  • mapDispatchToPropsFactories: 对 mapDispatchToProps 这个传入的参数的类型选择一个合适的方法。
  • mergePropsFactories: 对 mergeProps 这个传入的参数的类型选择一个合适的方法。
  • selectorFactory: 以上3个只是简单的返回另一个合适的处理方法,它则执行这些处理方法,并且对结果定义了如何比较的逻辑。

三个 mapxxx 参数,用来对 mapStateToPropsmapDispatchToProps 进行参数校验,并且通过 match 返回如下形式函数,作为 connectAdvanced 参数:

1
2
3
4
5
let initProxySelector = (mapToProps, methodName) => (dispatch, {displayName}) => {
const proxy = function mapToPropsProxy(...) {...}
// proxy 各种包装
return proxy;
}

后面解释为什么这么包装。 connect 核心部分就是高阶函数的封装:

connectAdvanced 高阶函数

简要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
export default function connectAdvanced (
selectorFactory,
{
//... 各种参数配置
} = {}
) {
const Context = context;
return function wrapWithConnect(WrappedComponent) {
// pure 决定 shouldComponentUpdate 是否进行 shallowEqual 浅比较,默认 true
const {pure} = connectOptions;
// 即 React.Component
let OuterBaseComponent = Component
let FinalWrappedComponent = WrappedComponent
class Connect extends OuterBaseComponent {
constructor(props) {
// 这两个定义的方法,后面介绍
this.selectDerivedProps = makeDerivedPropsSelector()
this.selectChildElement = makeChildElementSelector()
// bind this
this.renderWrappedComponent = this.renderWrappedComponent.bind(this)
}
// value 为 context, 既 provider中的 {storeState: store.getState(),store}
renderWrappedComponent(value) {
const {
storeState,
store
} = value
// 定义wrapperProps为this.props
let wrapperProps = this.props
let forwardedRef
// forwardRef为真时, Connect组件提供了forwardedRef = {ref}
if (forwardRef) {
// wrapperProps为props中的wrapperProps
wrapperProps = this.props.wrapperProps
forwardedRef = this.props.forwardedRef
}
//@1 导出 props
let derivedProps = this.selectDerivedProps(
storeState,
wrapperProps,
store
)
//@2 返回最终的组件, 传入最终的 props 和 ref -> 看selectChildElement发放
return this.selectChildElement(derivedProps, forwardedRef)
}
render() {
// 默认情况下公用的 ReactReduxContext
const ContextToUse = this.props.context || Context
return (
<ContextToUse.Consumer> {
this.renderWrappedComponent
} </ContextToUse.Consumer>
)
}
}
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
if (forwardRef) {
// 转发 Ref (React 16 ref 转发语法)
const forwarded = React.forwardRef(function forwardConnectRef(props,ref) {
return
<Connect
wrapperProps = {props}
forwardedRef = {ref}/>
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
// 静态方法补充
return hoistStatics(Connect, WrappedComponent)
}
}

connectAdvanced,本身上返回了一个包装了 <Context.Consumer> 的高阶组件,并且其内部已经用 React.forwardRef 解决了高阶组件的 ref 转发问题,利用 hoist-non-react-statics 工具解决了高阶组件静态方法复制的问题 hoistStatics(Connect, WrappedComponent)

如何优化?

到这里,react-redux 的机制大体就完整了。store 的每次更新都会触发 context 的变动,从而引起订阅了 context 的组件重新渲染。那么我们来思考两个问题:

  1. 每次 store 变动,都会触发根组件 setState 从而导致 re-render。我们知道当父组件 re-render 后一定会导致子组件 re-render 然而,引入 react-redux 并没有这个副作用,这是如何处理的?
  2. 不同的子组件,需要的只是 store 上的一部分数据,如何在 context 发生变化后,仅仅影响那些用到变化部分 context 的组件?

this.props.children

针对第一个问题,其实解决很简单。源码中,Provider 返回的是 <Context.Provider>this.props.children</Context> 这样当 Provider 调用 setState 的时候,由于 this.props.children 本质上是没有变化的,所以 Provider 下的所有组件都不会触发 re-render。简单来说如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class A1 extends React.Component {
componentDidMount() {
setTimeout(() => {
console.log('re-render')
this.setState({});
}, 1000)
}
render() {
return this.props.children;
}
}
class A2 extends React.Component {
render() {
console.log('A2 ')
return <span>A2</span>
}
}
// 渲染节点 <A1><A2 /><A1/>

运行结果如下,可以看到 this.props.children 下的内容都不会 re-render
a1.png

(如果没有使用 this.props.children 的写法,直接嵌套的话,那么当 Context 变动后,所有子组件都会 re-render 没有达到很好地预期效果)

(旧版本是通过对 connect 的组件都加入了 store.subscribe 达到的局部相应更新)

makeDerivedPropsSelector && makeChildElementSelector

针对第二个问题,我们可以在 renderWrappedComponent 中的这两个方法找到答案。

makeChildElementSelector

从上面的源码中,我们看出最终返回的组件就是调用了这个函数,带函数接受了最终需要渲染的 propsref 值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function makeChildElementSelector() {
// 定义props, ref, element变量
let lastChildProps, lastForwardRef, lastChildElement
// 返回function
return function selectChildElement(childProps, forwardRef) {
// 判断新旧 props, hre, elelment是否相同
if (childProps !== lastChildProps || forwardRef !== lastForwardRef) {
// 如果不同重新赋值
lastChildProps = childProps
lastForwardRef = forwardRef
lastChildElement = (
// return FinalWrappedComponent, 改变props和ref
<FinalWrappedComponent
{...childProps}
ref = {forwardRef}
/>
)
}
// react组件
return lastChildElement
}
}

作用很简单,就是判断 props ref 是否有变化,有变化就返回一个新的组件,没有则返回之前的。这个函数接受的最终版本的 derivedProps 就是由 makeDerivedPropsSelector 确定的。

makeDerivedPropsSelector

makeDerivedPropsSelector 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
function makeDerivedPropsSelector() {
// 闭包储存上一次的执行结果
let lastProps
let lastState
let lastDerivedProps
let lastStore
let sourceSelector
// storeState props store
return function selectDerivedProps(state, props, store) {
// props和state都和之前相等 直接返回上一次的结果
if (pure && lastProps === props && lastState === state) {
return lastDerivedProps
}
// 当前store和lastStore不等,更新lastStore
if (store !== lastStore) {
lastStore = store
// 终于调用 selectorFactory 了
sourceSelector = selectorFactory(
store.dispatch,
selectorFactoryOptions
)
}
// 更新数据
lastProps = props
lastState = state
// 返回的就是最终的包含所有相应的 state 和 props 的结果
const nextProps = sourceSelector(state, props)
// 最终的比较
if (lastDerivedProps === nextProps) {
return lastDerivedProps
}
lastDerivedProps = nextProps
return lastDerivedProps
}
}

先判断了当前传入的props (组件的props) 和 state (redux store 的state) 跟以前的是否全等,如果全等就不需要更新了;
如果不等,则调用高阶函数 selectFactory 得到 sourceSelector 函数,sourceSelector 会将当前 storeState 和 props 合并成一个 props。之后再次判断这个最终 props 和上一次的是否全等。
看下 selectFactory 源码:

selectFactory
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
export default function finalPropsSelectorFactory(
// redux store的store.dispatch
dispatch,
// 3种已经确定了的处理方法
{ initMapStateToProps, initMapDispatchToProps, initMergeProps, ...options }
) {
// 调用initProxySelector得到proxy function, proxy包含mapToProps, dependsOnOwnProps属性
const mapStateToProps = initMapStateToProps(dispatch, options)
const mapDispatchToProps = initMapDispatchToProps(dispatch, options)
// mergePropsProxy为function
// 返回值为 connect(mapstate,mapdispatch,function mergeProps(){})()中mergeProps的返回值
const mergeProps = initMergeProps(dispatch, options)
if (process.env.NODE_ENV !== 'production') {
verifySubselectors(
mapStateToProps,
mapDispatchToProps,
mergeProps,
options.displayName
)
}
const selectorFactory = options.pure
? pureFinalPropsSelectorFactory
: impureFinalPropsSelectorFactory
// 默认pure问题true,因此执行 pureFinalPropsSelectorFactory(...)
return selectorFactory(
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
options
)
}

首先这里执行了之前封装好的 initProxySelector 函数,返回一个 proxy 函数。

接下来执行 selectorFactory 函数,也就是 pureFinalPropsSelectorFactory 函数 (pure 默认值为 true)。关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
export function pureFinalPropsSelectorFactory(
// 接受3个proxy方法
mapStateToProps,
mapDispatchToProps,
mergeProps,
dispatch,
// 接受3个比较方法
{ areStatesEqual, areOwnPropsEqual, areStatePropsEqual }
) {
/* ...定义变量保存之前的数据(闭包)... */
function handleFirstCall(firstState, firstOwnProps) {
/* ...定义第一次执行数据比较的方法,也就是简单的赋值给上面定义的闭包变量... */
}
function handleNewPropsAndNewState() {
/* 当state和props都有变动时的处理方法 */
}
function handleNewProps() {
/* 当state无变动,props有变动时的处理方法 */
}
function handleNewState() {
/* 当state有变动,props无变动时的处理方法 */
}
// 后续数据比较的方法
function handleSubsequentCalls(nextState, nextOwnProps) {
// 浅比较
const propsChanged = !areOwnPropsEqual(nextOwnProps, ownProps)
// 全等比较
const stateChanged = !areStatesEqual(nextState, state)
// 更新数据
state = nextState
ownProps = nextOwnProps
// 当发生不相等的3种情况(关键)
if (propsChanged && stateChanged) return handleNewPropsAndNewState()
if (propsChanged) return handleNewProps()
if (stateChanged) return handleNewState()
// 比较都相等,直接返回旧值
return mergedProps
}
return function pureFinalPropsSelector(nextState, nextOwnProps) {
return hasRunAtLeastOnce
? handleSubsequentCalls(nextState, nextOwnProps)
: handleFirstCall(nextState, nextOwnProps)
}
}

pureFinalPropsSelectorFactory 函数返回的就是最外层 sourceSelector 函数。

函数主要是对比 storeState 和组件 props 的数据,并返回合并结果。(在函数 handleSubsequentCalls,其中 areStatesEqual 为严格相等(===)比较。其余比较均为 shallowEqual 浅比较)
新老对比分为如下几种情况:

  1. storeState 和 props 都相等,直接返回第一次 handleFirstCall 函数执行结果的 mergedProps

handleFirstCall:
必定执行 mapStateToProps(state, ownProps)
必定执行 mapDispatchToProps(dispatch, ownProps)
合并结果 mergedProps

  1. storeState 不等 props 不等

handleNewPropsAndNewState
必定执行 mapStateToProps(state, ownProps),因为 state 有变动
只有订阅了ownProps,才会执行 mapDispatchToProps,因为 state 变动与 mapDispatchToProps 无影响
合并结果 mergedProps

  1. storeState 相等 props 不等,执行 handleNewProps

handleNewProps
只有订阅了ownProps,才会执行 mapStateToProps, 因为 state 无变动。
只有订阅了ownProps,才会执行 mapDispatchToProps,因为 state 变动与 mapDispatchToProps 无影响。
合并结果 mergedProps

  1. storeState 不等,props 相等

handleNewState
必定执行 mapStateToProps(state, ownProps)
对新的 stateProps 与上一次结果做浅比较判断,浅比较失败才重新计算 mergedProps,否则返回旧的结果。(就相当月 PureComponent 优化了)。

(第 4 种和第 1 种差别就在于 props 和 state 一旦同时变化就注定要更新组件)。

总结

react-redux 核心就这么多。其本质就是利用 context 连接 store 和 react。通过 connect 来决定哪儿些组件需要响应 redux 的数据流变化,并做了通用的方案做优化。
这里借鉴一张导图:

a2.png

参考资料