之前的文章我们讲解了数据流的作用,还有 redux 的源码。现在思考,如何将 redux 应用到 react 上面呢?
首先我们想到 store 可能被多个组件依赖和影响,为此我们应当借助 context
来存储 store 数据。将 store 存储在根元素上,通过 context 进行数据的传递。为了实现这一步,react-redux 提供了 <Provider>
:
Provider
Provider 简单代码如下:
|
|
我们可以看到,在 Provider 准备完毕后,将 state.store
作为 context 的值。并且监听 store 的变化,如果有变化则调用 setState
来更新 context。这里用到的 context 是 React 16+
之后的新版本写法。之后会对其进行介绍。
现在我们子组件想要使用 store
数据的就可以包装在 <Context.Customer>
里面即可。当然这中间少不了一些优化和功能的增强,这些方法都被封撞在了 connect
中。
connect
首先看下 connect 的用法:
|
|
connect 简化版代码如下:
|
|
先说明一下该函数的参数:
- connectHOC: 一个重要组件,用于执行已确定的逻辑,渲染最终组件,后面会详细说。
- mapStateToPropsFactories: 对 mapStateToProps 这个传入的参数的类型选择一个合适的方法。
- mapDispatchToPropsFactories: 对 mapDispatchToProps 这个传入的参数的类型选择一个合适的方法。
- mergePropsFactories: 对 mergeProps 这个传入的参数的类型选择一个合适的方法。
- selectorFactory: 以上3个只是简单的返回另一个合适的处理方法,它则执行这些处理方法,并且对结果定义了如何比较的逻辑。
三个 mapxxx 参数,用来对 mapStateToProps
和 mapDispatchToProps
进行参数校验,并且通过 match 返回如下形式函数,作为 connectAdvanced 参数:
|
|
后面解释为什么这么包装。 connect
核心部分就是高阶函数的封装:
connectAdvanced 高阶函数
简要代码如下:
|
|
connectAdvanced
,本身上返回了一个包装了 <Context.Consumer>
的高阶组件,并且其内部已经用 React.forwardRef
解决了高阶组件的 ref 转发问题,利用 hoist-non-react-statics
工具解决了高阶组件静态方法复制的问题 hoistStatics(Connect, WrappedComponent)
。
如何优化?
到这里,react-redux 的机制大体就完整了。store 的每次更新都会触发 context
的变动,从而引起订阅了 context
的组件重新渲染。那么我们来思考两个问题:
- 每次
store
变动,都会触发根组件setState
从而导致re-render
。我们知道当父组件re-render
后一定会导致子组件re-render
然而,引入 react-redux 并没有这个副作用,这是如何处理的? - 不同的子组件,需要的只是
store
上的一部分数据,如何在context
发生变化后,仅仅影响那些用到变化部分context
的组件?
this.props.children
针对第一个问题,其实解决很简单。源码中,Provider
返回的是 <Context.Provider>this.props.children</Context>
这样当 Provider 调用 setState
的时候,由于 this.props.children 本质上是没有变化的,所以 Provider 下的所有组件都不会触发 re-render
。简单来说如下:
|
|
运行结果如下,可以看到 this.props.children
下的内容都不会 re-render
(如果没有使用 this.props.children 的写法,直接嵌套的话,那么当 Context 变动后,所有子组件都会 re-render 没有达到很好地预期效果)
(旧版本是通过对 connect 的组件都加入了 store.subscribe 达到的局部相应更新)
makeDerivedPropsSelector && makeChildElementSelector
针对第二个问题,我们可以在 renderWrappedComponent
中的这两个方法找到答案。
makeChildElementSelector
从上面的源码中,我们看出最终返回的组件就是调用了这个函数,带函数接受了最终需要渲染的 props
和 ref
值:
|
|
作用很简单,就是判断 props ref 是否有变化,有变化就返回一个新的组件,没有则返回之前的。这个函数接受的最终版本的 derivedProps
就是由 makeDerivedPropsSelector
确定的。
makeDerivedPropsSelector
makeDerivedPropsSelector 代码如下:
|
|
先判断了当前传入的props (组件的props) 和 state (redux store 的state) 跟以前的是否全等,如果全等就不需要更新了;
如果不等,则调用高阶函数 selectFactory
得到 sourceSelector
函数,sourceSelector
会将当前 storeState 和 props 合并成一个 props。之后再次判断这个最终 props 和上一次的是否全等。
看下 selectFactory
源码:
selectFactory
|
|
首先这里执行了之前封装好的 initProxySelector
函数,返回一个 proxy 函数。
接下来执行 selectorFactory
函数,也就是 pureFinalPropsSelectorFactory
函数 (pure 默认值为 true)。关键代码如下:
|
|
pureFinalPropsSelectorFactory
函数返回的就是最外层 sourceSelector
函数。
函数主要是对比 storeState 和组件 props 的数据,并返回合并结果。(在函数 handleSubsequentCalls
,其中 areStatesEqual
为严格相等(===)比较。其余比较均为 shallowEqual
浅比较)
新老对比分为如下几种情况:
- storeState 和 props 都相等,直接返回第一次
handleFirstCall
函数执行结果的 mergedProps
handleFirstCall
:
必定执行 mapStateToProps(state, ownProps)
必定执行 mapDispatchToProps(dispatch, ownProps)
合并结果 mergedProps
- storeState 不等 props 不等
handleNewPropsAndNewState
:
必定执行 mapStateToProps(state, ownProps),因为 state 有变动
只有订阅了ownProps,才会执行 mapDispatchToProps
,因为 state 变动与 mapDispatchToProps 无影响
合并结果 mergedProps
- storeState 相等 props 不等,执行
handleNewProps
:
handleNewProps
:
只有订阅了ownProps,才会执行 mapStateToProps
, 因为 state 无变动。
只有订阅了ownProps,才会执行 mapDispatchToProps
,因为 state 变动与 mapDispatchToProps 无影响。
合并结果 mergedProps
- storeState 不等,props 相等
handleNewState
:
必定执行 mapStateToProps(state, ownProps)
对新的 stateProps 与上一次结果做浅比较判断,浅比较失败才重新计算 mergedProps,否则返回旧的结果。(就相当月 PureComponent 优化了)。
(第 4 种和第 1 种差别就在于 props 和 state 一旦同时变化就注定要更新组件)。
总结
react-redux 核心就这么多。其本质就是利用 context 连接 store 和 react。通过 connect 来决定哪儿些组件需要响应 redux 的数据流变化,并做了通用的方案做优化。
这里借鉴一张导图: