Redux源码分析(1) — createStore
2023-04-22·10min
type
Post
summary
status
Published
category
tags
slug
date
Apr 22, 2023
password
icon
Redux源码分析
Redux是一个优秀的JS状态管理工具,它继承了Flux单向数据流的思想,并在此基础上强调三个基本原则:
- 唯一数据源
- 保持状态只读
- 数据改变只能通过纯函数完成
利用Redux可以帮助我们构建状态可控的web应用,下面就让我们来了解下它是如何工作的吧!
redux项目结构
本文基于redux4.2.0版本,项目结构如下:
utils
为了便于后续的源码阅读,我们简单先过一遍Redux的工具函数。
actionTypes.js
randomString函数用来生成一个随机字符串。
ActionTypes函数向外暴露了三个预定义的action类型,供redux本身使用。
isPlainObject.js
isPlainObject函数用来判断一个对象是否是一个纯对象。
至于什么是纯对象呢?简单来说就是通过new Object()或者字面量的方式构建出来的对象
warning.js
warning函数用来输出警告信息,特别需要注意的是这里对console作了一层判断,原因是console有兼容性问题,ie8及其以下不支持。
redux的入口 — index.js
index.js是整个redux项目的入口,它的作用就是对外暴露可供开发者使用的API。
index文件有两个需要了解的地方:
- 导出的__DO_NOT_USE__ActionTypes 从导入的源头来看,正是utils/actionTypes.js文件中的ActionTypes对象。 根据这个变量的命名,可以知道这个变量是用来帮助开发者做类型检查的,防止使用了redux自定义的action类型。
- 空函数isCrushed 根据源码的注释以及变量命名,这个函数是用来检查redux代码是否被压缩。 我们知道js代码压缩后,函数名/变量名都会被简化。 这里就做了判断,如果是在开发环境下,使用了压缩过的redux代码,就会报错。
redux的核心 — createStore.js
createStore的参数
我们知道,使用redux的第一步,就是要通过createStore来创建一个store。
createStore接收三个参数,分别是reducer,preloadedState和enhancer。
但在实际使用中,我们可以只传reducer和enhancer,跳过不传preloadedState,redux是如何做到的呢?
答案是redux做了一层转换。
在createStore.js中可以找到下面这段代码:
当preloadedState的类型是function,且第三个参数是undefined时,就将preloadedState的值赋给enhancer,并将preloadedState重新设置为undefined。
另外是enhancer,中文可以翻译为增强器,它是一个函数,用来增强store。
createStore里有这么一行代码:
可以看到,如果传入了enhancer,就会调用它,并返回一个被增强过的store。
在redux中,常用的enhancer就是redux-thunk和redux-saga这些中间件,使用这些中间件需要搭配applyMiddleware使用,applyMiddleware的作用就是将这些中间件转化为能被redux调用的enhancer。
下面是一个使用redux-thunk的例子
createStore暴露的API
在createStore.js最后,可以看到向外暴露的API:
可以看到向外暴露了一个对象,对象里包括了几个方法,其中dispatch, subscribe, getState是比较常用的。
接下来正式解析createStore的源码
判断入参是否正确
这里分别做了一下几件事:
- 判断preloadedState和enhancer是否都是function,如果都是,认为开发者传入了多个enhancer,抛出异常,并提示用户使用compose函数把多个enhancer合并成一个。
- 如果preloadedState类型为function,且enhancer为undefined, 将preloadedState赋值给enhancer,并将preloadedState重新设置为undefined。
- 判断enhancer是否存在,如果存在且为function,调用它,并返回一个被增强过的store。
- 检查reducer的类型,如果不是function,抛出异常。
定义初始化变量
这里将入参重新赋值为变量,还声明了两个订阅者列表,供发布通知、取消订阅、订阅时使用。
另外声明了一个isDispatching作为一个锁来使用,这个锁的作用是防止在开发者reducer中非法调用dispatch。若没有这个锁,开发者如果在reducer中调用了dispatch,那么dispatch执行后又会触发reducer的执行,造成死循环。
dispatch
dispatch可以说是redux工作流的核心,通过它可以把action,reducer和store三位“主角”给串联起来。
请看dispatch相关代码:
dispatch首先进行三次判断:
- 判断action是否是一个纯对象
- 判断action是否有type属性
- 通过isDispatching判断当前是否有dispatch执行,如果有,抛出异常,提示用户不能在reducer中调用dispatch。
接着使用一个try-finally块来执行reducer
- 首先将isDispatching设为true,表示当前已存在dispatch工作流
- 将当前的reducer和action传入reducer,获取新的state
- 最后在finally里将isDispatching设为false,表示当前dispatch工作流结束
接着获取当前的订阅者列表,一一通知订阅者做数据更新,最后返回当前的action。
getState
getState比较简单,就是返回最新的currentState,而这个currentState在每次dispatch都会被更新。
同样为了保持数据的一致,通过isDispatching来判断当前是否正在执行reducer,如果是,抛出异常,提示用户不能在reducer中调用getState。
另外需要注意的是,getState返回的是currentState的引用,也就是说,我们是可以直接对store中的state进行修改的,并不需要dispatch(不知道算不算BUG)。
但直接修改state,违背了不可变对象的原则,也不会通知订阅者,所以我们还是需要使用dispatch来更新state。
subscribe
在注册订阅者前,首先做了两次判断:
- 判断订阅者是否是一个可执行的函数
- 通过isDispatching判断当前是否有dispatch执行,如果是,抛出异常,提示用户不能在reducer中调用subscribe。
将isSubscribed设置为true,表示已订阅
然后执行ensureCanMutateNextListeners函数,确保nextListeners和currentListeners不是同一个数组
然后并将订阅者添加到nextListeners中。
最后返回一个unsubscribe函数,用于取消订阅。
unsubscribe函数逻辑也差不多:
- 通过isSubscribed判断是否已取消订阅,如果已取消,直接返回。
- 通过isDispatching判断当前是否有dispatch执行,如果是,抛出异常,提示用户不能在reducer中调用unsubscribe。
将isSubscribed设置为false,表示已取消订阅。
然后执行ensureCanMutateNextListeners函数,确保nextListeners和currentListeners不是同一个数组
最后将订阅者从nextListeners中移除。
辅助函数ensureCanMutateNextListeners
这里重点讲一下ensureCanMutateNextListeners,这个函数在订阅和取消订阅时都有用到,用来判断当前的nextListeners和currentListeners是否是同一个数组。
如果是,则用slice方法将nextListeners设置为currentListeners的浅拷贝。
在dispatch的函数中,我们可以看到:
将listeners和currentListeners都指向nextListeners,最后通过一个for循环来通知订阅者
这里实际上使用的是nextListeners
在subscribe的函数中,我们可以看到:
订阅和取消订阅,实际上使用的也是nextListeners
订阅/取消订阅是操作nextListeners,发布通知也是操作nextListeners,那么就有个问题了,为什么要声明两个订阅者列表呢?要currentListeners有何用??
答案是:正是因为操作都是针对的nextListeners,所以我们才需要一个稳定的currentListeners
设想下面这个场景,并且设想只有nextListeners一个订阅者列表:
在这个DEMO执行完毕后,nextListeners数组的内容是 A、B、C 3个listener:
接着若调用了dispatch,则会触发下面这段逻辑。
当for循环,i=1时,通知到B,B就会执行解绑A的操作
如果说不存在currentListeners,自然也就没有ensureCanMutateNextListeners,那么就会直接修改nextListeners。
解绑完订阅者A后, for循环会继续走到i=2,这时候会发现,由于A被解绑,数组中的元素都前进了一位,也就是原本C的位置变成了D,那么就会触发通知到D,C被忽略了。
为了避免这个情况,才需要两个订阅者列表,在订阅/取消订阅时,都操作新的nextListeners,不影响正在通知订阅的listeners。
replaceReducer
replaceReducer在实际项目中使用的不多,用来替换reducer。
replaceReducer函数执行前会做一个判断:判断所传reducer是否为函数
通过条件判断之后,再将nextReducer赋值给currentReducer,并触发dispatch,更新state。
初始化state
在createStoro函数的最后,还有一段代码:
为什么要执行一次dispatch呢?
还记得在初始化变量时有这么一段代码吗
假如我们没有传入preloadedState,currentState自然就是undefined。
假如我们不初始化执行一次dispatch,那么state就拿不到reducer里的默认值,那么应用在第一次getState时,也拿不到默认值,后续dispatch的时候,也就没办法在默认值的基础上做更新了。
所以我们需要默认执行一次初始化,拿到所有reducer默认的state。
结语
createStore是Redux的核心,分析createStore代码能帮我们掌握redux最基础的工作流程,由于篇幅所限,redux其余的API分析会放在下一篇。