Redux的核心功能基本都在 createStrore 中,我们使用Redux也由此方法开始,可以说是最为重要的一个方法,下面我们就来了解一下createStore究竟是怎么工作的。
createStore 方法预览
方法的签名
首先来看 createStore 的方法签名:
function createStore(reducer, preloadedState, enhancer)
复制代码
方法接收三个参数:
- reducer (func) 纯函数,接收当前状态树 state 和 发起的 action 两个参数,reducer处理完后返回一个新的state。
- preloadedState (any) state树的初始值,虽然不限制类型,但一般使用对象来储存状态。
- enhancer (func) 函数,用来增强store的第三方扩展,也就是中间件。
方法的返回
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
复制代码
返回一个Store对象,对象暴露以上的方法给外部:
- 通过Store.getState() 方法使读取state树
- 通过Store.dispatch() 方法更新state树
- 通过Store.subscribe() 方法监听state树,subscribe方法还会返回一个unsubscribe()函数用来注销监听器
- 通过Store.replaceReducer() 方法来替换reducer。
方法的作用
创建并返回一个维护State树的Store对象,更新State树的唯一方法是调用 Store.dispatch(),一个Redux应用应该只有一个Store,如果需要模块化state树的处理逻辑,则可以编写多个reducer,使用Redux另一个API (combineReducers,后续会说到)合并成一个 reducer 作为参数传入 createStore。
详细解读 CreateStore
接下来看看详细代码:
方法的开头首先对传入的参数进行了类型判断:
/**
* 这里使得 createStore 可以忽略参数 preloadedState
* 从而可以这样调用: createStore(reducer,enhancer)
*/
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
/**
* 检查 enhancer 是否是函数
* 主要是应用于 Redux 中间件
*/
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
/**
* 暂且跳过这里 applyMiddleware 时再进行讲解
* 可以暂时这样认为:
* enhancer(createStore)(reducer, preloadedState) 这一系列函数执行完后
* 返回的也是一个Store对象 只不过这个对象被加工了
*/
return enhancer(createStore)(reducer, preloadedState)
}
/**
* 检查 reducer 是否是函数
*/
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
复制代码
接着初始化一些变量:
// 当前的 reducer 函数
// 主要用于 Store.replaceReducer() 替换 reducer 时使用
let currentReducer = reducer
// currentState 就是 store 中的状态树,这里先对它进行了初始化
// Store.getState() 返回此变量
let currentState = preloadedState
// 以下的两个变量主要用于 Store.subscribe()方法
// 当前的订阅者
let currentListeners = []
// 下一次 Store.dispatch() 更新state树 需要发出通知的订阅者
let nextListeners = currentListeners
// 是否正在调用 Store.dispatch() 方法
// Store.dispatch方法调用时,会禁止一些操作执行
let isDispatching = false
复制代码
接下来一个个看看Store暴露的接口方法:
Store.getState()
Store.getState() 是最简单的一个方法,直接返回当前的状态树,也就是 currentState 变量。
/**
* 返回当前的状态树 state
* 不允许在调用 Store.dispatch() 方法时获取状态树
*/
function getState() {
if (isDispatching) {
throw new Error(
'xxxxxx'
)
}
return currentState
}
复制代码
Store.dispatch()
Store.dispatch()方法,传入一个action对象,更新state树的唯一途径就是调用此方法。
调用例子:
Store.dispatch({
type: "ADD_TODO",
text: "Read the source code."
})
复制代码
同样,函数的开始先对入参合法性进行判断(因为JavaScript是弱类型语言- -、):
function dispatch(action) {
// 检查 action 是否为纯对象
// 纯对象即是通过 {} 或者 new Object() 创建的对象,其原型为 Object.prototype
if (!isPlainObject(action)) {
throw new Error(
'xxxxxx'
)
}
// 检查 action.type 是否存在
// type 字段是必须的
if (typeof action.type === 'undefined') {
throw new Error(
'xxxxx'
)
}
// 同一时刻只能执行一个 dispatch 函数
// 为了防止在 dispatch 函数嵌套调用 dispatch 函数
if (isDispatching) {
throw new Error('xxxx')
}
复制代码
入参类型都正确后,接着将当前的 state树 和传入的 action 传递给 reducer 执行 ,如何去更新state树的数据处理逻辑是由reducer决定的,reducer 执行相关的数据处理逻辑,返回新的state树,从而更新state树:
try {
// 设置变量
// 正在更新状态树 一些操作将被禁止
isDispatching = true
// 传入 state,action 交由 reducer 函数 执行
// reducer 返回新的 state
currentState = currentReducer(currentState, action)
} finally {
// 状态更新完成 设置为false
isDispatching = false
}
复制代码
由于state树的更新,需要向订阅监听的函数发出通知:
/*
* 更新了state树 向所有的订阅者发出通知
* 任何时刻 增加一个监听器时(调用Store.subscribe()) 首先会将监听器加到 nextListeners 数组中
* 直到下一个 dispatch 发起执行时 才会同步 currentListeners 和 nextListeners
* 然后对最新的订阅数组发出通知 执行所有订阅函数
*/
// 同步最新的订阅数组
const listeners = (currentListeners = nextListeners)
// 执行所有的监听函数
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
复制代码
函数的最后,返回了入参action:
// 返回了原有的入参 action
// 在应用redux中间件时 此返回将发挥极大的作用
return action
复制代码
这个返回其实对于调用者而言,并没有多大用处。它真正发挥作用的时候是在于扩展Redux中间件的时候,也就是applyMiddleware方法,后续会进一步解读。
dispatch方法完成的任务主要有三个:
- 检查入参action是否合法
- 将当前state树和入参action传递给reducer方法,由reducer计算新的state后,更新state树。
- 更新了state树,向所有订阅者发出通知,也就是执行所有监听函数。
Store.subscribe()
接下来看Store.subscribe(),方法的作用是订阅state树的变化,每次执行完dispatch方法后,都会触发监听,执行传入的监听函数。
调用例子:
// 注册一个监听函数 当state树更新时 输出最新的state值
Store.subscribe(()=>{
console.log(Store.getState())
})
复制代码
在对subscribe方法解读之前,先看看方法内部用到的一个函数 ensureCanMutateNextLisenteners。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
// slice() 不传入参数的话 就是拷贝整个数组,返回一个新的数组
nextListeners = currentListeners.slice()
}
}
复制代码
函数十分简单,判断当前订阅和下次订阅是否指向同一个数组,如果是的话就拷贝数组,使得 nextListeners 和 currentListneres 保存两份数据一样的数组。但是为什么要这样做呢?首先应该明确的是,不管是注册监听,还是注销监听,都只会在 nextListeners 上操作,直到 dispatch 被调用时,才会同步 currentListneres 和 nextListeners ,也就是 dispatch 中的 :
const listeners = (currentListeners = nextListeners)
复制代码
dispatch完成后 currentListeners 和 nextListeners 这两个变量就会指向同一个数组,在此之后,如果你注册注销监听,在 nextListeners 上操作的同时,势必也会影响到 currentListeners 这个变量,这样就混淆了 currentListeners 和 nextListeners 两个变量的作用,所以需要一个 ensureCanMutateNextLisenteners 函数,保证在nextListeners 上注册注销监听,都不会影响到 currentListeners 。具体调用场景看下面对subscribe方法的解读:
同样,subscribe 方法一开始也是对入参 listener 进行判断:
// 检查参数类型是否正确
if (typeof listener !== 'function') {
throw new Error('xxxx')
}
// 不允许 dispatch 函数正在执行的时候进行订阅
if (isDispatching) {
throw new Error(
'xxxx'
)
}
复制代码
接着用闭包保存了一个变量,用来标记监听函数是否正在监听
let isSubscribed = true
复制代码
因为要对 nextListeners 进行操作,所以调用了 ensureCanMutateNextLisenteners ,确保操作不会影响到 currentListeners
// 确认修改 nextListeners 时 不影响到 currentListeners
ensureCanMutateNextListeners()
// 将新的监听函数加入到下一次监听函数执行队列
nextListeners.push(listener)
复制代码
函数的最后,返回一个函数,用来注销监听:
// 返回一个取消监听的函数
return function unsubscribe() {
if (!isSubscribed) {
// 当然 取消订阅只能取消一次...
return
}
// 不允许在 dispatch 的时候取消监听函数
if (isDispatching) {
throw new Error(
'xxxxxxxxxxxx'
)
}
// 注销监听
isSubscribed = false
// 确认修改 nextListeners 时 不影响到 currentListeners
ensureCanMutateNextListeners()
// 删除这个监听函数,下一次 dispatch 时会生效
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
复制代码
subscribe 方法完成的任务只有两个:
- 注册监听
- 返回一个注销监听的函数
Store.replaceReducer()
接下来看看同样简单的 Store.replaceReducer() 方法:
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('xxxxx')
}
// 更新reducer
currentReducer = nextReducer
// 会触发一个Redux独有的action,来确定状态树是否能响应新的reducer变化
dispatch({
type: ActionTypes.REPLACE
})
}
复制代码
方法的作用就是替换reducer函数,达到热更新reducer的效果,一般很少用到。值得注意的是,方法之中 dispatch 了一个 action:
dispatch({
type: ActionTypes.REPLACE
})
复制代码
可见,Redux在实现内部自己也会触发一些action,具体的作用来看看 ActionTypes 这个常量的定义。
解读 ActionTypes.js
ActionTypes 来自 ./src/utils/actionTypes.js ,源码如下:
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`, // str
REPLACE: `@@redux/REPLACE${randomString()}`, // str
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}` // fun
}
export default ActionTypes
复制代码
就是通过 randomString 生成三个任意的,不可预测的 action.type 名称,这些 action 仅供 Redux内部使用。例如上面使用到了 REPLACE 这个action,整个createStore代码的最后部分,可看到还发起了一个 type为 INIT 的 action :
// 偷偷发起了一个action
dispatch({
type: ActionTypes.INIT
})
// 然后就是正常的导出:
return {
//....
}
复制代码
为什么Redux需要使用到这些私有action呢? 其实非常简单,原因在于Redux对reducer计算新的state树的时候有严格的要求,为了严格约束reducer,需要触发一定的action来检测返回的新state树是否符合预期,具体要求是这样的:
- 对于任何未知的操作,reducer需要返回当前状态
- 如果当前状态未定义,即传入的state为undefined,则reducer需要返回初始状态
也就是以下两种情况:
- dispatch( undefined , action ) 传入的初始state为undefined时,reducer需要返回一个初始state值。
- dispatch( crruentState , unkownAction ) 传入一个未知action时,reducer必须返回原有的state。
第一点是也为初始化state树,使得不做任何更新之前,都能通过getState()访问到state树。 第二点中,如果 dispatch 了一个未知的action,reducer什么也没有返回,即函数默认返回了undefined,则在 dispatch 在更新state树的时候:
currentState = currentReducer(currentState, action)
复制代码
就会设置当前状态 currentState = 'undefined' ,导致丢失state的值。
最后
createStore源码不难阅读,最重要的是要理解state,action,reducer等概念的作用。方法返回的Store对象基本包含了我们使用Redux的方法。但其中没有包含更多细节,比如我们要如何使用多个Reducer划分数据处理逻辑,如何应用中间件等,这些操作还要看Redux提供的其他方法。