相信用过React的小伙伴对于Redux一定不陌生,A Predictable State Container for JS Apps
,这是官方文档对于Redux的定义,即一款适用于JS Apps的可预测的状态管理容器,简单来讲,Redux就是用来存放一些我们在项目中不同组件都需要使用的一些公共数据,并且通过Redux内置的方法在不同组件中去修改或者读取这些公共数据,并且实际上Redux和React并不是一种绑定的形式,也就是说Redux是一个通用的解决方案,不受制于框架类型的限制,只不过通常我们在使用Vue的时候,会习惯使用与Vue绑定的Vuex来管理我们的公共数据,所以慢慢地React和Redux,Vue和Vuex就被绑定到一起使用了。
其实对于Redux而言,是有专门适用于React框架的react-redux库来使用的,只不过今天的内容是对Redux的解析并且实际上react-redux也是对Redux的封装从而可以更加简单方便的在React框架中使用,所以弄清楚了Redux本身后,再去理解react-redux就会很轻松。
Redux使用
createStore(reducer, preloadedState, enhancer)
创建一个store对象,方法包含三个参数,reducer是我们传入的修改state数据的回调函数,且reducer的返回值必须是一个state,即我们定义的state数据(必须),preloadedState是我们传入的默认state数据(可选),enhancer是我们传入的增强器,Redux内部提供的唯一的内置增强器为applyMiddleware(可选),关于增强器的概念在后续我们会一步一步说明,这里暂时只需要现有一个概念即可,也就是我们可以在创建store对象时传入增强器回调函数用来改变一些操作行为
store.getState()
获取store对象的state数据
store.dispatch(action)
官方定义的改变store对象state数据的唯一行为,如果我们想对state数据进行操作,使用dispatch()方法是官方唯一推荐的方法
import { createStore } from 'redux'
// 单独定义一个state集合的目的在于如果后续我们需要添加更多的数据,则只需要在num里面添加对应的属性和数据即可
// 保证在当前的这个store中始终只存在一个数据源
const num = {
obj: {
id: 1,
name: 'xiaohong',
age: 20,
}
}
// 自定义的reducer方法
const countReducer = (state = num.obj, action) => {
switch (action.type) {
case 'increment':
state.age += 1
return state
case 'decrement':
state.age -= 1
return state
default:
return state
}
}
// 这里说明一下,虽然方法提供了第二个参数用来初始化我们的state数据,但是实际上我们通常会在传入的reducer回调函数中的state参数传入我们的初始化state数据,最终也同样会作为我们store对象的默认state数据,至于原因,后续在源码解析里面我们会看到对应的处理方式
const store = createStore(countReducer)
// 在Redux中,官方定义的可以改变state数据的行为只有一种,那就是通过dispatch(action)方法,其中我们传入的action对象里面的type
// 会对应reducer中的switch()方法来判断不同的type执行不同的函数体
store.dispatch({type: 'increment'})
// 我们可以通过store对象内置的subscribe()方法来观测store的变化,每一次的dispatch()执行都会触发,同时执行我们传入的回调函数,并且观测方法的调用可以累加,也就是当我们写了多个subscribe()方法时,每一次的改变都会依次触发所有的subscribe()方法和执行传入的回调参数
store.subscribe(() => {
// 通过getState()内置方法可以获取store对象的state数据
console.log(store.getState())
})
console.log(store.getState()) // { id: 1, name: 'xiaohong', grade: 21 }
Redux源码解析
在了解了Redux的基本使用之后,正式的开始我们对于Redux源码的分析,同时在学习源码的过程中,也会对Redux内置的其他常用方法进行说明
Redux项目结构
这里我们截取了redux源码src目录下的文件内容,可以看到主要分为三个部分
types文件夹:存放相关的类型定义,使用TypeScript实现
utils文件夹:存放一些公共方法
和上面两个文件夹处于同层的5个redux核心方法文件以及index.js入口文件
我们可以看到其实整个redux核心代码并不是很多,而且文件的结构和命名也使得我们可以很清楚的知道这个文件实现的是redux的哪一个部分,那么接下来我们就开始对这些文件进行进一部分解析。
types文件夹
存放redux实现过程中需要的各种类型定义
utils文件夹
存放一些需要使用的公共方法
actionTypes.ts
const randomString = () =>
Math.random().toString(36).substring(7).split('').join('.')
const ActionTypes = {
INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
REPLACE: `@@redux/REPLACE${/* #__PURE__ */ randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
export default ActionTypes
文件结构很简单,最终暴露出来两个action类型,用于通过createStore()方法创建store对象时,对于state数据的初始化操作
randomString()方法的作用是生成一个随机的经过处理的字符串用于拼接在初始化类型字符串的后面,这里我想着重解析一下这个随机字符串处理的方法
Math.random():生成一个随机数这个是一个经常会使用到的方法
toString(36):注意这里的toString()方法并不是我们经常所使用的处于Object原型上的转化为字符串的toString()方法,而是Number.prototype.toString(redix),目的是返回执行Number类型的字符串表示形式,允许传入参数,用于数字到字符串的转换的基数(2 - 36),也就是通常我们想要将数字转换为其他的进制表示的形式,这里的处理是将随机数转换为36进制的字符串表示形式
Substring(7).split(’’).join(’.’):截取字符串从索引值为7开始到字符串末尾,且通过split()方法对字符串的每一个字母中间添加"."并返回新字符串,这里substring(7)设定为7的原因在于经过对随机数的36进制转化之后,从索引值为7开始截取一直到字符串末尾,可以保证生成的最终字符串为一个固定位数的字符串,如y.k.x.d.u.c
这种形式
isPlainObject.ts
export default function isPlainObject(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false
let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
return Object.getPrototypeOf(obj) === proto
}
判断对象是否是简单对象
什么是简单对象:只有通过{}或new Object()创建的对象才能称为简单对象
这里关于getPrototypeOf()方法还需要再了解清楚一点
这里再解释一下Object.getPrototypeOf()方法:方法返回指定对象的原型(内部prototype属性的值)
warning.ts
export default function warning(message: string): void {
if (typeof console !== 'undefined' && typeof console.error === 'function') {
console.error(message)
}
try {
throw new Error(message)
} catch (e) {}
}
这里的代码很简单,目的就是为了打印错误信息,不过需要提的一点是在函数内部首先做了一个关于console和console.error的类型判断,目的是为了适配IE浏览器,因为在IE8及更低的版本当中是不支持console相关的打印语句的,只能说还是大神考虑的周到,说实话不是看到这个代码再去查了一下关于console方面的信息的话,我根本不知道IE8还有这一层限制。
为了适配低版本的浏览器,当不能执行console语句时,直接选择通过抛出异常的方式来显示错误信息
Redux核心方法实现部分
index.js
Redux的入口文件
import __DO_NOT_USE__ActionTypes from './utils/actionTypes'
function isCrushed() {}
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
) {
warning(
'You are currently using minified code outside of NODE_ENV === "production". ' +
'This means that you are running a slower development build of Redux. ' +
'You can use loose-envify (https://github.com/zertosh/loose-envify) for browserify ' +
'or setting mode to production in webpack (https://webpack.js.org/configuration/mode/) ' +
'to ensure you have the correct code for your production build.'
)
}
export {
createStore,
combineReducers,
bindActionCreators,
applyMiddleware,
compose,
__DO_NOT_USE__ActionTypes
}
首先可以看到入口文件最后导出了store的基本API,外部使用可以直接通过import来引入对应的方法即可
这里还有一个定义的空函数 isCrushed() 和 if条件判断语句:
首先是对于node环境的判断,如果处于开发环境,则函数执行结束,不会打印错误信息
其次是对于函数名类型的定义以及其名称的判断,这里需要解释一下为什么需要对函数名称进行处理,是因为我们在打包项目的时候需要进行压缩,经过压缩过后的代码其中的函数名称会被替换为一个字母,这样就很好理解为什么需要三个判断共同起作用了,当node环境不是生产环境且代码未经过压缩时,调用warn()方法打印警告信息。
function isCrushed() {}
if (
process.env.NODE_ENV !== 'production' &&
typeof isCrushed.name === 'string' &&
isCrushed.name !== 'isCrushed'
)
function d(){}"string"==typeof d.name&&"isCrushed"!==d.name
(这里引用了https://segmentfault.com/a/1190000016460366文章中对于该函数的解释)
在结尾的export导出内容当中,除了我们熟悉的方法以外,还多出了一个__DO_NOT_USE__ActionTypes
,往上找可以看到这个就是我们在前面介绍的actionType.js文件的导出内容,也就是初始的action类型,目的应该是为了使得开发者可以检查一下自己命名的action中的type类型是不是和redux默认的初始化类型有所冲突,虽然这个概率非常低,但是可以看出作者思考的真的是很全面
createStore.ts
createStore()方法的实现文件,用来创建一个store对象
在官网上对于createStore(reducer, preloadedState, enhancer)方法的介绍包含三个参数
Reducer(Function):即用来操作state数据的回调函数
preloadedState(Object):初始传入的state数据,平常很少用到,我们习惯将初始数据通过reducer回调函数中的第一个参数state指定默认值的方式传入,前面提到,这两种方式最终都会成为store对象里面state的初始值
enhancer(Function):增强器,用来曾江Redux,并且通过增强器可以使用一些中间件来实现更多的修改行为的操作
if (
(typeof preloadedState === 'function' && typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function. See https://redux.js.org/tutorials/fundamentals/part-4-store#creating-a-store-with-enhancers for an example.'
)
}
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState as StoreEnhancer<Ext, StateExt>
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error(
`Expected the enhancer to be a function. Instead, received: '${kindOf(
enhancer
)}'`
)
}
return enhancer(createStore)(
reducer,
preloadedState as PreloadedState<S>
) as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
}
if (typeof reducer !== 'function') {
throw new Error(
`Expected the root reducer to be a function. Instead, received: '${kindOf(
reducer
)}'`
)
}
在createStore()方法内部,首先是四个判断条件,我们一个一个来分析一下
第一个if
条件判断:当传入的三个参数的第二个参数preloadedstate
为函数类型且第三个参数enhancer
也为函数类型时,或第三个参数enhancer
为函数类型且存在第四个为函数类型的参数时,抛出异常(Redux只允许传入一个增强器,不允许传入多个,且第二个参数state的初始值不能是函数形式)
第二个if
条件判断:当第二个参数preloadedstate
为函数类型且不存在第三个参数时,将第二个函数当做增强器使用,且将原本函数定义的第二个参数preloadedState置为undefined
第三个if
条件判断:当传入的enhancer
不为空时,如果类型不是函数类型,抛出异常,反之调用增强器函数,将createStore作为参数,将reducer,preloadedState作为返回值函数的参数
第四个if
条件判断:当传入的enhancer
不是函数类型时,抛出异常
(上述四个条件判断的目的,是为了确保传入createStore()方法的参数都是正确的类型)
let currentReducer = reducer
let currentState = preloadedState as S
let currentListeners: (() => void)[] | null = []
let nextListeners = currentListeners
let isDispatching = false
currentReducer:保存当前传入的reducer
currentState:保存当前传入的preloadedState(由于大多数state是、初始值不是通过传参,故当前值大部分情况下为undefined)
currentListeners:当前订阅者列表,初始为空
nextListeners:保存订阅回调函数(即当我们使用subscribe(callback)方法是传入的回调函数)
isDispatching:作为一个状态判断来使用,即确保数据的修改是串行而不是并行,也就是同一时间只允许一个action对state进行操作,true
表示正在执行action中
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
function getState(): S {
if (isDispatching) {
throw new Error(
'You may not call store.getState() while the reducer is executing. ' +
'The reducer has already received the state as an argument. ' +
'Pass it down from the top reducer instead of reading it from the store.'
)
}
return currentState as S
}
getState()方法,也就是我们在实际项目中使用的store.getState()来获取store对象中的state数据,当在执行action操作时,不允许同时执行getState()来读取state数据
function subscribe(listener: () => void) {
if (typeof listener !== 'function') {
throw new Error(
`Expected the listener to be a function. Instead, received: '${kindOf(
listener
)}'`
)
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
let isSubscribed = true
ensureCanMutateNextListeners()
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
return
}
if (isDispatching) {
throw new Error(
'You may not unsubscribe from a store listener while the reducer is executing. ' +
'See https://redux.js.org/api/store#subscribelistener for more details.'
)
}
isSubscribed = false
ensureCanMutateNextListeners()
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
currentListeners = null
}
}
subscribe()方法,也就是我们在实际项目中使用的store.subscribe(callback)来实现对于state数据的订阅,当通过dispatch()改变state数据时,就会遍历订阅列表,执行其中的回调函数
当传入的listener不是函数类型或者正在执行action操作时,都会直接抛出异常
设置初始订阅判断:isSubscribed = true 表示已经处于订阅状态
ensureCanMutateNextListeners():执行浅拷贝操作,出现问题时可以将nextListeners作为临时的订阅列表进行操作
nextListeners.push(listener):将订阅回调函数推入新的订阅者列表数组保存
我们可以看到,subscribe()方法的返回值的函数命名是unscribe,也就是说当我们调用subscribe()时,处于订阅状态,如果使用subscribe()()实际上就取消了这一次的订阅,响应的将当前订阅者列表置空,从新的订阅者列表数组当中取出当前传入的listener订阅回调函数,即完成了本次的取消订阅操作
function dispatch(action: A) {
if (!isPlainObject(action)) {
throw new Error(
`Actions must be plain objects. Instead, the actual type was: '${kindOf(
action
)}'. You may need to add middleware to your store setup to handle dispatching other values, such as 'redux-thunk' to handle dispatching functions. See https://redux.js.org/tutorials/fundamentals/part-4-store#middleware and https://redux.js.org/tutorials/fundamentals/part-6-async-logic#using-the-redux-thunk-middleware for examples.`
)
}
if (typeof action.type === 'undefined') {
throw new Error(
'Actions may not have an undefined "type" property. You may have misspelled an action type string constant.'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
dispatch()方法,即我们在实际项目中使用的官方推荐的唯一可以对state数据进行操作的行为store.dispatch(action)
当传入的action不是一个简单对象(简单对象的定义我们在解析isPlainObject()方法时提过,只有通过{}或new Object()出案件的对象才是简单对象),抛出异常
当传入的action不存在type属性时,抛出异常,因为我们在修改state数据时就是根据type来进入不同条件的代码块中执行的
当正在执行action行为时,抛出异常,Redux规定action的执行只能是串行而不能并行
修改isDispatching状态为true,表示正在执行action行为
currentState = currentReducer(currentState, action):覆盖保存通过action行为修改过后的state数据
执行结束后修改isDispatching状态为false,表示action行为执行结束
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
function replaceReducer<NewState, NewActions extends A>(
nextReducer: Reducer<NewState, NewActions>
): Store<ExtendState<NewState, StateExt>, NewActions, StateExt, Ext> & Ext {
if (typeof nextReducer !== 'function') {
throw new Error(
`Expected the nextReducer to be a function. Instead, received: '${kindOf(
nextReducer
)}`
)
}
;(currentReducer as unknown as Reducer<NewState, NewActions>) = nextReducer
dispatch({ type: ActionTypes.REPLACE } as A)
return store as unknown as Store<
ExtendState<NewState, StateExt>,
NewActions,
StateExt,
Ext
> &
Ext
}
顺带提一下replaceReducer()
方法,之所以是顺带是因为这个方法我们在实际项目中几乎不会去使用,它的作用是替换掉当前的reducer
使用新的reducer
,内部的代码也很简单,判断新的reducer是否是函数形式,将新的reducer赋值给currentReducer来替换掉原先的reducer,执行dispatch()且type类型为actionType.js
文件中定义的默认ActionTypes.REPLACE
类型
dispatch({ type: ActionTypes.INIT } as A)
const store = {
dispatch: dispatch as Dispatch<A>,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
} as unknown as Store<ExtendState<S, StateExt>, A, StateExt, Ext> & Ext
return store
其实在阅读到这里之前我都一直存在一个疑问,那就是state的默认值问题,通常情况下我们是不通过第二个参数preloadedState
来传入state的初始数值的,那么首先 let currentState = preloadedState as S
这段代码赋值后currentState的值肯定为undefined,我们在调用getState()方法时,可以看到其实方法本质就是return currentState
所以我们才可以读取到state的内容。而currentState = currentReducer(currentState, action)
这段代码是存在于dispatch()方法内部的。
所以就产生了一个问题,当我在不使用dispatch()方法时,就调用getState(),且通过reducer的方式传入state的默认值不通过preloadedState的参数传入,那么是如何获取到初始的state值的呢。
实际上,在createStore()方法的末尾,会执行一次dispatch()操作,并且传入的tye值为默认定义的ActionTypes.INIT
,关于这个默认类型我们前面有专门介绍过,最终的结果是生成一个随机的字符串,那么这个type跟我们传入的reducer()方法内的type是没有对应匹配的,故直接返回state,也就是我们在reducer中传入的初始state的数值,故即使在使用时不调用任何dispatch()方法的前提下,也能通过getState()方法获取到state的数值,疑问自然而言也就解开了。
combineReducers.ts
combineReducers(reducers)方法的实现文件,用来合并多个reducer
方法接收一个reducers对象集合(Object)作为参数,如{one: reducerOne, two: reducerTwo}
const reducerKeys = Object.keys(reducers)
const finalReducers: ReducersMapObject = {}
for (let i = 0; i < reducerKeys.length; i++) {s
const key = reducerKeys[i]
if (process.env.NODE_ENV !== 'production') {
if (typeof reducers[key] === 'undefined') {
warning(`No reducer provided for key "${key}"`)
}
}
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
reducerKeys:存储传入的reducers集合的属性名,如果是以{one: reducerOne, two: reducerTwo}方式传入,则属性名集合为[“one, two”],且会作为state数据中每一个reducer传入的默认state参数的集合名。如果以{reducerOne, reducerTwo}方式传入,则属性名集合为[“reducerOne”, “reducerTwo”],且会作为state数据中每一个reducer传入的默认state参数的集合名。
finalReducers:存储最终的reducer集合
for()循环存储的reducerKeys属性名集合,当前环境不是生产环境且当传入的reducers集合当中不存在reducerKeys中的属性名时,抛出警告异常
当reducers中对应属性名的类型是函数类型时,将该reducer存入finalReducers中
return function combination(
state: StateFromReducersMapObject<typeof reducers> = {},
action: AnyAction
) {
if (shapeAssertionError) {
throw shapeAssertionError
}
if (process.env.NODE_ENV !== 'production') {
const warningMessage = getUnexpectedStateShapeWarningMessage(
state,
finalReducers,
action,
unexpectedKeyCache
)
if (warningMessage) {
warning(warningMessage)
}
}
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const actionType = action && action.type
throw new Error(
`When called with an action of type ${
actionType ? `"${String(actionType)}"` : '(unknown type)'
}, the slice reducer for key "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
console.log(nextState)
return hasChanged ? nextState : state
}
我们可以看到,其实combineReducers()方法的返回值是一个函数。
首先我们可以看到返回的函数接收两个参数,一个是state,一个是action,我们需要分析一下这两个参数是在什么地方传入的
const combine = combineReducer({reducerOne, reducerTwo})
const store = createStore(combine)
我们可以看到最终我们将合并后的reducer集合作为参数传入createStore()的参数,也就是createStore()方法中接收的reducer参数,在上面分析createStore.js文件的时候,我们分析过方法内部的state初始化实际上是在最后调用了dispatch({ type: ActionTypes.INIT } as A),在dispatch()方法内部,
我们可以看到是通过currentState = currentReducer(currentState, action)
这一行代码来进行赋值操作的,所以答案揭晓,我们在combineReducers()方法内部的返回函数中的参数就是这么传递过来的
let hasChanged = false
const nextState: StateFromReducersMapObject<typeof reducers> = {}
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
const previousStateForKey = state[key]
const nextStateForKey = reducer(previousStateForKey, action)
if (typeof nextStateForKey === 'undefined') {
const actionType = action && action.type
throw new Error(
`When called with an action of type ${
actionType ? `"${String(actionType)}"` : '(unknown type)'
}, the slice reducer for key "${key}" returned undefined. ` +
`To ignore an action, you must explicitly return the previous state. ` +
`If you want this reducer to hold no value, you can return null instead of undefined.`
)
}
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
hasChanged =
hasChanged || finalReducerKeys.length !== Object.keys(state).length
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)