You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
function match(arg, factories, name) {
// 循环执行factories,这里的factories也就是mapStateToProps和mapDisPatchToProps两个文件中暴露出来的处理函数数组
for (let i = factories.length - 1; i >= 0; i--) {
// arg也就是mapStateToProps或者mapDispatchToProps
// 这里相当于将数组内的每个函数之星了一遍,并将我们的mapToProps函数作为参数传进去
const result = factories[i](arg)
if (result) return result
}
}
写在前面
我在读React-Redux源码的过程中,很自然的要去网上找一些参考文章,但发现这些文章基本都没有讲的很透彻,很多时候就是平铺直叙把API挨个讲一下,而且只讲某一行代码是做什么的,却没有结合应用场景和用法解释清楚为什么这么做,加上源码本身又很抽象,函数间的调用关系非常不好梳理清楚,最终结果就是越看越懵。我这次将尝试换一种解读方式,由最常见的用法入手,
结合用法,提出问题,带着问题看源码里是如何实现的,以此来和大家一起逐渐梳理清楚React-Redux的运行机制。
文章用了一周多的时间写完,粗看了一遍源码之后,又边看边写。源码不算少,我尽量把结构按照最容易理解的方式梳理,努力按照浅显的方式将原理讲出来,但架不住代码结构的复杂,很多地方依然需要花时间思考,捋清函数之间的调用关系并结合用法才能明白。文章有点长,能看到最后的都是真爱~
水平有限,难免有地方解释的不到位或者有错误,也希望大家能帮忙指出来,不胜感激。
React-Redux在项目中的应用
在这里,我就默认大家已经会使用Redux了,它为我们的应用提供一个全局的对象(store)来管理状态。
那么如何将Redux应用在React中呢?想一下,我们的最终目的是实现跨层级组件间通信与状态的统一管理。所以可以使用Context这个特性。
而这些都需要自己手动去做,React-Redux将上边的都封装起来了。让我们通过一段代码看一下React-Redux的用法:
首先是在React的最外层应用上,包裹Provider,而Provider是React-Redux提供的组件,这里做的事情相当于上边的第一步
第二步中的订阅,已经分别在Provider和connect中实现了
再看应用内的子组件。如果需要从store中拿数据或者更新store数据的话(相当于上边的第三步和第四步),需要用connect将组件包裹起来:
mapStateToProps 用于建立组件和store中存储的状态的映射关系,它是一个函数,第一个参数是state,也就是redux中存储的顶层数据,第二个参数是组件自身的props。返回一个对象,对象内的字段就是该组件需要从store中获取的值。
mapDispatchToProps用于建立组件和store.dispatch的映射关系。它可以是一个对象,也可以是一个函数,当它是一个函数的时候,第一个参数就是dispatch,第二个参数是组件自身的props。
mapDispatchToProps的对象形式如下:
当不传mapStateToProps的时候,当store变化的时候,不会引起组件UI的更新。
当不传mapDispatchToProps的时候,默认将dispatch注入到组件的props中。
以上,如果mapStateToProps 或者mapDispatchToProps传了ownProps,那么在组件自身的props变化的时候,这两个函数也都会被调用。
React-Redux做了什么
我们先给出结论,说明React-Redux做了什么工作:
如何做的
有了上边的结论,但想必大家都比较好奇究竟是怎么实现的,上边的几项工作都是协同完成的,最终的表象体现为下面几个问题:
接下来,带着这些问题来一条一条地分析源码。
Provider是怎么把store放入context中的
先从Provider组件入手,代码不多,直接上源码
所以结合代码看这个问题:Provider是怎么把store放入context中的,很好理解。
Provider最主要的功能是从props中获取我们传入的store,并将store作为context的其中一个值,向下层组件下发。
但是,一旦store变化,Provider要有所反应,以此保证将始终将最新的store放入context中。所以这里要用订阅来实现更新。自然引出Subscription类,通过该类的实例,将onStateChange监听到一个可更新UI的事件
this.notifySubscribers
上:组件挂载完成后,去订阅更新,至于这里订阅的是什么,要看Subscription的实现。这里先给出结论:本质上订阅的是
onStateChange
,实现订阅的函数是:Subscription类之内的trySubscribe
再接着,如果前后的state不一样,那么就去通知订阅者更新,onStateChange就会执行,Provider组件就会执行下层组件订阅到react-redux的更新函数。当Provider更新完成(componentDidUpdate),会去比较一下前后的store是否相同,如果不同,那么用新的store作为context的值,并且取消订阅,重新订阅一个新的Subscription实例。保证用的数据都是最新的。
我猜想应该有一个原因是考虑到了Provider有可能被嵌套使用,所以会有这种在Provider更新之后取新数据并重新订阅的做法,这样才能保证每次传给子组件的context是最新的。
Provider将执行触发listeners执行的函数订阅到了store。
Subscription
我们已经发现了,Provider组件是通过Subscription类中的方法来实现更新的,而过一会要讲到的connect高阶组件的更新,也是通过它来实现,可见Subscription是React-Redux实现订阅更新的核心机制。
Subscription就是将页面的更新工作和状态的变化联系起来,具体就是listener(触发页面更新的方法,在这里就是handleChangeWrapper),通过trySubscribe方法,根据情况被分别订阅到store或者Subscription内部。放入到listeners数组,当state变化的时候,listeners循环执行每一个监听器,触发页面更新。
说一下trySubscribe中根据不同情况判断直接使用store订阅,还是调用addNestedSub来实现订阅的原因。因为前者的场景是Provider将listener订阅到store中,此时的listeners数组内其实是每个connect内部的checkForUpdates函数(后边会讲到)。后者是connect内部将checkForUpdates放到listeners数组中,实际上是利用Provider中传过来的Subscrption实例来订阅,保证所有被connect的组件都订阅到一个Subscrption实例上。
如何向组件中注入state和dispatch
将store从应用顶层注入后,该考虑如何向组件中注入state和dispatch了。
正常顺序肯定是先拿到store,再以某种方式分别执行这两个函数,将store中的state和dispatch,以及组件自身的props作为mapStateToProps和mapDispatchToProps的参数,传进去,我们就可以在这两个函数之内能拿到这些值。而它们的返回值,又会再注入到组件的props中。
说到这里,就要引出一个概念:selector。最终注入到组件的props是selectorFactory函数生成的selector的返回值,所以也就是说,mapStateToProps和mapDispatchToProps本质上就是selector。
生成的过程是在connect的核心函数connectAdvanced中,这个时候可以拿到当前context中的store,进而用store传入selectorFactory生成selector,其形式为
通过形式可以看出:selector就相当于mapStateToProps或者mapDispatchToProps,selector的返回值将作为props注入到组件中。
从mapToProps到selector
标题的mapToProps泛指mapStateToProps, mapDispatchToProps, mergeProps
结合日常的使用可知,我们的组件在被connect包裹之后才能拿到state和dispatch,所以我们先带着上边的结论,单独梳理selector的机制,先看connect的源码:
connect实际上是createConnect,createConnect也只是返回了一个connect函数,而connect函数返回了connectHOC的调用(也就是connectAdvanced的调用),再继续,connectAdvanced的调用最终会返回一个wrapWithConnect高阶组件,这个函数的参数是我们传入的组件。所以才有了connect平常的用法:
大家应该注意到了connect函数内将mapStateToProps,mapDispatchToProps,mergeProps都初始化了一遍,为什么要去初始化而不直接使用呢?带着疑问,我们往下看。
初始化selector过程
先看代码,主要看initMapStateToProps 和 initMapDispatchToProps,看一下这段代码是什么意思。
mapStateToPropsFactories 和 mapDispatchToPropsFactories都是函数数组,其中的每个函数都会接收一个参数,为mapStateToProps或者mapDispatchToProps。而match函数的作用就是循环函数数组,mapStateToProps或者mapDispatchToProps作为每个函数的入参去执行,当此时的函数返回值不为假的时候,赋值给左侧。看一下match函数:
match循环的是一个函数数组,下面我们看一下这两个数组,分别是mapStateToPropsFactories 和 mapDispatchToPropsFactories:
(下边源码中的whenMapStateToPropsIsFunction函数会放到后边讲解)
mapStateToPropsFactories
实际上是让
whenMapStateToPropsIsFunction
和whenMapStateToPropsIsMissing
都去执行一次mapStateToProps,然后根据传入的mapStateToProps的情况来选出有执行结果的函数赋值给initMapStateToProps。单独看一下whenMapStateToPropsIsMissing
wrapMapToPropsConstant返回了一个函数,接收的参数是我们传入的() => ({}),函数内部调用了入参函数并赋值给一个常量放入了constantSelector中,
该常量实际上就是我们不传mapStateToProps时候的生成的selector,这个selector返回的是空对象,所以不会接受任何来自store中的state。同时可以看到constantSelector.dependsOnOwnProps = false,表示返回值与connect高阶组件接收到的props无关。
mapDispatchToPropsFactories
没有传递mapDispatchToProps的时候,会调用whenMapDispatchToPropsIsMissing,这个时候,constantSelector只会返回一个dispatch,所以只能在组件中接收到dispatch。
当传入的mapDispatchToProps是对象的时候,也是调用wrapMapToPropsConstant,根据前边的了解,这里注入到组件中的属性是
bindActionCreators(mapDispatchToProps, dispatch)的执行结果。
现在,让我们看一下whenMapStateToPropsIsFunction这个函数。它是在mapDispatchToProps与mapStateToProps都是函数的时候调用的,实现也比较复杂。这里只单用mapStateToProps来举例说明。
再提醒一下:下边的mapToProps指的是mapDispatchToProps或mapStateToProps
wrapMapToPropsFunc返回的实际上是initProxySelector函数,initProxySelector的执行结果是一个代理proxy,可理解为将传进来的数据(state或dispatch, ownProps)代理到我们传进来的mapToProps函数。proxy的执行结果是proxy.mapToProps,本质就是selector。
页面初始化执行的时候,dependsOnOwnProps为true,所以执行proxy.mapToProps(stateOrDispatch, ownProps),也就是detectFactoryAndVerify。在后续的执行过程中,会先将proxy的mapToProps赋值为我们传入connect的mapStateToProps或者mapDispatchToProps,然后在依照实际情况组件是否应该依赖自己的props赋值给dependsOnOwnProps。(注意,这个变量会在selectorFactory函数中作为组件是否根据自己的props变化执行mapToProps函数的依据)。
总结一下,这个函数最本质上做的事情就是将我们传入connect的mapToProps函数挂到proxy.mapToProps上,同时再往proxy上挂载一个dependsOnOwnProps来方便区分组件是否依赖自己的props。最后,proxy又被作为initProxySelector的返回值,所以初始化过程被赋值的initMapStateToProps、initMapDispatchToProps、initMergeProps实际上是initProxySelector的函数引用,它们执行之后是proxy,至于它们三个proxy是在哪执行来生成具体的selector的我们下边会讲到。
现在,回想一下我们的疑问,为什么要去初始化那三个mapToProps函数?目的很明显,就是准备出生成selector的函数,用来放到一个合适的时机来执行,同时决定selector要不要对ownProps的改变做反应。
创建selector,向组件注入props
准备好了生成selector的函数之后,就需要执行它,将它的返回值作为props注入到组件中了。先粗略的概括一下注入的过程:
下面我们需要从最后一步的注入开始倒推,来看selector是怎么执行的。
注入的过程发生在connect的核心函数connectAdvanced之内,先忽略该函数内的其他过程,聚焦注入过程,简单看下源码
在注入过程中,有一个很重要的东西:
selectorFactory
。这个函数就是生成selector的很重要的一环。它起到一个上传下达的作用,把接收到的dispatch,以及那三个mapToProps函数,传入到selectorFactory内部的处理函数(pureFinalPropsSelectorFactory 或 impureFinalPropsSelectorFactory)中,selectorFactory的执行结果是内部处理函数的调用。而内部处理函数的执行结果就是将那三种selector(mapStateToProps,mapDispatchToProps,mergeProps)执行后合并的结果。也就是最终要传给组件的props
下面我们看一下selectorFactory的内部实现。为了清晰,只先一下内部的结构
可以看出来,selectorFactory内部会决定在什么时候生成新的props。下面来看一下完整的源码
至此,我们搞明白了mapToProps函数是在什么时候执行的。再来回顾一下这部分的问题:如何向组件中注入state和dispatch,让我们从头梳理一下:
传入mapToProps
首先,在connect的时候传入了mapStateToProps,mapDispatchToProps,mergeProps。再联想一下用法,这些函数内部可以接收到state或dispatch,以及ownProps,它们的返回值会传入组件的props。
基于mapToProps生成selector
需要根据ownProps决定是否要依据其变化重新计算这些函数的返回值,所以会以这些函数为基础,生成代理函数(proxy),代理函数的执行结果就是selector,上边挂载了dependsOnOwnProps属性,所以在selectorFactory内真正执行的时候,才有何时才去重新计算的依据。
将selector的执行结果作为props传入组件
这一步在connectAdvanced函数内,创建一个调用selectorFactory,将store以及初始化后的mapToProps函数和其他配置传进去。selectorFactory内执行mapToProps(也就是selector),获取返回值,最后将这些值传入组件。
大功告成
React-Redux的更新机制
React-Redux的更新机制也是属于订阅发布的模式。而且与Redux类似,一旦状态发生变化,调用listener更新页面。让我们根据这个过程抓取关键点:
不着急看代码,我觉得先用文字描述清楚这些关键问题,不再一头雾水地看代码更容易让大家理解。
更新谁?
回想一下平时使用React-Redux的时候,是不是只有被connect过并且传入了mapStateToProps的组件,会响应store的变化?
所以,被更新的是被connect过的组件,而connect返回的是connectAdvanced,并且并且connectAdvanced会返回我们传入的组件,
所以本质上是connectAdvanced内部依据store的变化更新自身,进而达到更新真正组件的目的。
订阅的更新函数是什么?
这一点从connectAdvanced内部订阅的时候可以很直观地看出来:
订阅的函数是
checkForUpdates
,重要的是这个checkForUpdates做了什么,能让组件更新。在connectAdvanced中使用useReducer内置了一个reducer,这个函数做的事情就是在前置条件(状态变化)成立的时候,dispatch一个action,来触发更新。如何判断状态变化?
这个问题很好理解,因为每次redux返回的都是一个新的state。直接判断前后的state的引用是否相同,就可以了
connect核心--connectAdvanced
connectAdvanced是一个比较重量级的高阶函数,上边大致说了更新机制,但很多具体做法都是在connectAdvanced中实现的。源码很长,逻辑有一些复杂,我写了详细的注释。看的过程需要思考函数之间的调用关系以及目的,每个变量的意义,带着上边的结论,相信不难看懂。
看完了源码,我们整体概括一下React-Redux中被connect的组件的更新机制:
这其中有三个要素必不可少:
connectAdvanced函数内从context中获取
store
,再获取subscription
实例(可能来自context或新创建),然后创建更新函数checkForUpdates
,当组件初始化,或者store、Subscription实例、selector变化的时候,订阅或者重新订阅。在每次组件更新的时候,检查一下store是否变化,有变化则通知更新,
实际上执行checkForUpdates,本质上调用内置reducer更新组件。每次更新导致selector重新计算,所以组件总是能获取到最新的props。所以说,更新机制的最底层
是通过connectAdvanced内置的Reducer来实现的。
总结
至此,围绕常用的功能,React-Redux的源码就解读完了。回到文章最开始的三个问题:
store变化,被connect的组件也会更新的
现在我们应该可以明白,这三个问题对应着React-Redux的三个核心概念:
它们协同工作也就是React-Redux的运行机制:Provider将数据放入context,connect的时候会从context中取出store,获取mapStateToProps,mapDispatchToProps,使用selectorFactory生成Selector作为props注入组件。其次订阅store的变化,每次更新组件会取到最新的props。
阅读源码最好的办法是先确定问题,有目的性的去读。开始的时候我就是硬看,越看越懵,换了一种方式后收获了不少,相信你也是。
The text was updated successfully, but these errors were encountered: