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 参数,用来对 mapStateToProps 和 mapDispatchToProps 进行参数校验,并且通过 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

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

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

参考资料