"Redux 是 JavaScript 状态容器,提供可预测化的状态管理",这是redux官网上的简介,基于此研究下redux内部到底是如何管理js状态的,并且实现一个简易版的Redux。
既然是个状态容器,就命名为store(为啥不叫container?因为官方叫store),再声明个类似构造函数的createStore。管理状态,很容易就想到这个store需要一个setter,一个getter,另外状态可预测,那么应该有个watcher。
const createStore = (initialState) => {
let state = initialState
let listeners = []
const subscribe = (listener) => {
listeners.push(listener)
}
const changeState = (curState) => {
state = curState
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
}
const getState = () => {
return state
}
return { subscribe, changeState, getState }
}
不难就写出上面这个createStore,state存储各种状态,changeState对应setter,getState对应getter,再利用发布订阅实现状态变更时触发所有listener。
使用的话也很简单,如下:
let state = { count: 0, info: { name: 'person' } }
const store = createStore(state)
store.subscribe(() => {
console.log(
'count:',
store.getState().count,
'name:',
store.getState().info.name
)
})
store.changeState({ ...store.getState(), info: { name: '小李' } })
store.changeState({ ...store.getState(), count: 1 })
一个极简单的状态管理器就ok了,完整代码可见demo1
初版中,changeState其实是开放度极高的,每次修改state时,可以任意变换state中各个状态即各个属性的数据类型、值等等,因此要有限制,降低这个开放度,让state有计划的变更。就用switch/case来声明这种计划性任务。以实现count的增减操作为例:
将changeState中直接修改state的操作,替换成计划性变更state,这个变更暂时声明为format,作为createStore的形参。format的形参包含了当前的state以及当前的变更计划action。
const createStore = (format, initialState) => {
let state = initialState
let listeners = []
const subscribe = (listener) => {
listeners.push(listener)
}
const changeState = (action) => {
state = format(state, action) // 使用format()代替直接修改state,通过协商好的action限制变更操作
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
}
const getState = () => {
return state
}
return { subscribe, changeState, getState }
}
初始计划是实现count的增减,action描述了每次的变更计划,type为计划名,payload为本次变更的相关参数,返回的是变更后的state
const format = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.payload }
break
case 'DECREMENT':
return { count: state.count - action.payload }
break
default:
return state
}
}
let initialState = { count: 0}
const store = createStore(format, initialState)
store.subscribe(() => {
const latestState = store.getState()
console.log('count:', latestState.count)
})
store.changeState({ type: 'INCREMENT', payload: 1 })
store.changeState({ type: 'DECREMENT', payload: 2 })
store.changeState({ type: 'ANYTHING_ELSE', payload: 666 }) // 无效计划
这样提前定义好每次状态变更的计划,就让状态变更可控可预测,如计划之外的ANYTHING_ELSE则是无效的,返回的是原state。将变更计划的format换个名字,叫reducer吧,:),完整代码可见demo2
reducer的任务可见就是处理上一状态的state并返回变更后的state,但如果认知上不同功能类型的计划都放在一个reducer中,是既不美观也不易维护的,老司机们此时可能都想到了,将不同功能的reducer抽离出来,做成单独的reducer,并且要有个类似于命名空间的字段或者属性来和state进行对应,方便后续一类变更维护一类state。
增加一类变更,声明为msgReducer
const countReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.payload }
break
case 'DECREMENT':
return { count: state.count - action.payload }
break
default:
return state
}
}
const msgReducer = (state, action) => {
switch (action.type) {
case 'MODIFY_ID':
return { ...state, id: action.payload.id }
break
case 'MODIFY_CONTENT':
return { ...state, content: action.payload.content }
break
default:
return state
}
}
初始state多维护个状态msg,且将两类状态用“命名空间”分隔开
let initialState = { counter: { count: 0 }, msg: { id: 0, content: 'test' } }
createStore只是换了形参名
const createStore = (reducer, initialState) => {
...
const dispatch = (action) => {
state = reducer(state, action) // format->reducer
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
}
...
}
此时要做的就是怎么将多个reducer合并得到一个rootReducer(demo中声明为reducers),来统一区分action,进行计划变更。合并reducer的函数声明为combineReducers,期望返回的也是个reducer。
// 也用”命名空间“将reducer分隔开,要和state对应起来
const reducers = combineReducers({ counter: countReducer, msg: msgReducer })
const combineReducers=(originReducers) => {
//返回的也是reducer,以供createStore中dispatch直接使用
return function (state, action) {
...
}
}
combineReducers的实现思路也很明确,遍历所有的reducer,通过命名空间,找到上一状态state树中当前reducer维护的state,并通过当前reducer变更得到新的state,最后将新的state同步到state树
const combineReducers=(originReducers) => {
const rKeys = Object.keys(originReducers)
return function (state, action) {
const nextState = {}
for (let i = 0; i < rKeys.length; i++) {
const key = rKeys[i]
// 这里注意,如dispatch的是countReducer的action
// 那么msgReducer会走到default分支,返回的是上一状态的msg,即没有变更
nextState[key] = originReducers[key](state[key], action)
}
return nextState
}
}
完整代码可见demo3
上一版中state初始值是统一维护,放在一个state树中的,但更理想的是和对应的reducer放在一起维护,加个容错即可,以countReducer为例
let initialState = { count: 0 }
const countReducer = (state, action) => {
if (!state) {
state = initialState
}
switch (action.type) {
case 'INCREMENT':
return { count: state.count + action.payload }
break
case 'DECREMENT':
return { count: state.count - action.payload }
break
default:
return state
}
}
export default countReducer
因为state初始值不统一在createStore前维护,那么在createStore时initialState为undefined,即总的state树初始值为undefined。第一次dispatch时就会走到reducer新加的容错中,因此create时触发一次dispatch,达到初始化的目的
const createStore=(reducer, initialState) => {
let state = initialState // 没有统一维护初始值,为undefined
...
const dispatch = (action) => {
state = reducer(state, action) // 首次dispatch,state为undefined
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
}
// 实质走到了reducer的default分支,会拿到各个reducer内部维护的初始state来初始总的state树
dispatch({ type: Symbol() })
...
}
combineReducers中返回的rootReducer也得加上容错
const combineReducers = (originReducers) => {
const rKeys = Object.keys(originReducers)
return function (state, action) {
const nextState = {}
for (let i = 0; i < rKeys.length; i++) {
const key = rKeys[i]
const preState = state ? state[key] : undefined // 新加容错
// 这里注意,每个单独的reducer维护的state初始值,不用加上命名空间,是因为在这合并到新的state树时,会使用reducer的命名空间
nextState[key] = originReducers[key](preState, action)
}
return nextState
}
}
使用时可以打印下state树,看看长啥样
const reducers = combineReducers({ counter: countReducer, msg: msgReducer })
const store = createStore(reducers)
console.log('initialState:', store.getState())
...
完整代码可见demo4
redux中还有个重要的概念 ,即中间件,下面这段话摘自redux中文官网
相对于 Express 或者 Koa 的 middleware,Redux middleware 被用于解决不同的问题,但其中的概念是类似的。它提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
Express 或者 Koa的中间件,没接触过的可以查阅下相关文档,这里不再赘述。其实简单来说,中间件可以让redux状态变更更可控。
中间件的本质是重写dispatch,增加一些额外操作。参考官方文档提及的例子,想要在dispatch时打印状态变更前后的state树,以及捕获变更时的异常,以此为目的分别重写dispatch
const reducers = combineReducers({ counter: countReducer, msg: msgReducer })
const store = createStore(reducers)
const next = store.dispatch
// 打印日志
store.dispatch = (action) => {
console.log('preState:', store.getState())
next(action)
console.log('nextState', store.getState())
}
// 捕获异常
store.dispatch = (action) => {
try {
console.log('preState:', store.getState())
next(action)
console.log('nextState', store.getState())
} catch (e) {
console.log('error:', e)
}
}
将二者抽成单独的函数,并且组合在一起使用
...
const loggerMiddleware = (action) => {
console.log('preState:', store.getState())
next(action)
console.log('nextState', store.getState())
}
const exceptionMiddleware = (action) => {
try {
loggerMiddleware(action)
} catch (e) {
console.log('error:', e)
}
}
store.dispatch = exceptionMiddleware
exceptionMiddleware中直接用了loggerMiddleware,说明此时中间件之间存在耦合,那需要解耦操作一下,利用闭包包一层
...
const loggerMiddleware = (next) => (action) => {
console.log('preState:', store.getState())
next(action)
console.log('nextState', store.getState())
}
const exceptionMiddleware = (next) => (action) => {
try {
next(action)
} catch (e) {
console.log('error:', e)
}
}
store.dispatch = exceptionMiddleware(loggerMiddleware(next))
在某些中间件中还会用到store,基于函数式编程**,也将store剥离,用闭包的方式再包一层
...
const loggerMiddleware = (store) => (next) => (action) => {
console.log('preState:', store.getState())
next(action)
console.log('nextState', store.getState())
}
const exceptionMiddleware = (store) => (next) => (action) => {
try {
next(action)
} catch (e) {
console.log('error:', e)
}
}
const logger = loggerMiddleware(store)
const exception = exceptionMiddleware(store)
store.dispatch = exception(logger(next))
完整代码可见demo5
用过koa的老司机应该知道,koa源码中对于中间件会用一个compose函数去进行合并,形成经典的洋葱模型,可参考本人对于koa源码的研究。在redux中其实也有组合中间件的概念,声明一个applyMiddleware函数,预期是能组合各个中间件并且将createStore转换为新的createStore,形式如下
const newCreateStore = applyMiddleware(loggerMiddleware,exceptionMiddleware)(createStore)
先将applyMiddleware的架子搭出来,闭包的第一层返回的是形参为原createStore的overrideCreateStoreFunc,最里层是返回同createStore一样,形参是reducers, initialState的newCreateStore
const applyMiddleware = (...middlewares) => {
return function overrideCreateStoreFunc(oldCreateStore) {
return function newCreateStore(reducers, initialState) {
...
return store
}
}
}
newCreateStore里的任务也很明确,第一是给各个中间件注入store,第二是组合各个中间件
const applyMiddleware = (...middlewares) => {
return function overrideCreateStoreFunc(oldCreateStore) {
return function newCreateStore(reducers, initialState) {
const store = oldCreateStore(reducers, initialState)
const dispatch = store.dispatch
const injectStoreMiddlewares = middlewares.map((middleware) =>
middleware(store)
)
// 组合中间件
const combineMiddlewares = compose(injectStoreMiddlewares)
store.dispatch = combineMiddlewares(dispatch)
return store
}
}
}
compose其实是实现demo5中注入store后中间件的组合
// demo5
store.dispatch = exception(logger(next))
先看看源码中的实现,很巧妙,但也需要一定的功力(´▽`)
const compose = (middlewares) => {
return middlewares.reduce(
(a, b) =>
(...args) =>
a(b(...args))
)
}
如果难以理解,换一种方式实现
const compose = (middlewares,dispatch) => {
let dispatchTmp = dispatch
middlewares.reverse().map(middleware => {
dispatchTmp = middleware(dispatchTmp)
})
return dispatchTmp
}
完整代码可见demo6
demo6中applyMiddleware返回overrideCreateStoreFunc,换个角度可以理解为对createStore的加强,将它作为createStore的一个形参enhancer,如果存在则对createStore进行增强再返回。另外,原生Redux支持createStore(reducer, enhancer)这样使用,其实也就是加了个容错。
const createStore = (reducer, initialState, enhancer) => {
if (typeof initialState === 'function' && typeof enhancer === 'undefined') {
enhancer = initialState
initialState = undefined
}
if (enhancer && typeof enhancer === 'function') {
return enhancer(createStore)(reducer, initialState)
}
...
}
源码的createStore还暴露出一个replaceReducer,用来变更reducer,实现很简单,替换加更新state树
const createStore = (reducer, initialState, enhancer) => {
...
const replaceReducer = (nextReducer) => {
reducer = nextReducer
// 同初始化,走到reducer的default分支,取到reducer内部维护的state初始值,以此重置state树
dispatch({ type: Symbol() })
}
...
return { replaceReducer }
}
既然可以订阅那就需要有取消订阅,安排
const createStore = (reducer, initialState, enhancer) => {
...
const subscribe = (listener) => {
listeners.push(listener)
return function unsubscribe() {
const curIndex = listeners.indexOf(listener)
listeners.splice(curIndex, 1)
}
}
...
return { subscribe }
}
之前applyMiddleware时是将原store整个注入到中间件,但这样中间件里就可以重写store的一些原生api了,很明显这种破坏性的行为不符合预期,因此要避免这种情况
const applyMiddleware = (...middlewares) => {
return function overrideCreateStoreFunc(oldCreateStore) {
return function enhancer(reducers, initialState) {
const store = oldCreateStore(reducers, initialState)
// 只暴露出getter
const limitStore = { getState: store.getState }
const dispatch = store.dispatch
const injectStoreMiddlewares = middlewares.map((middleware) =>
middleware(limitStore)
)
const combineMiddlewares = compose(injectStoreMiddlewares)
store.dispatch = combineMiddlewares(dispatch)
return store
}
}
}
先看下相关概念在中文官网中的介绍
bindActionCreator:
把一个 value 为不同 action creator 的对象,转成拥有同名 key 的对象。同时使用
dispatch
对每个 action creator 进行包装,以便可以直接调用它们。惟一会使用到
bindActionCreators
的场景是当你需要把 action creator 往下传到一个组件上,却不想让这个组件觉察到 Redux 的存在,而且不希望把dispatch
或 Redux store 传给它。
Action Creator:
就是一个创建 action 的函数。不要混淆 action 和 action creator 这两个概念。Action 是一个信息的负载,而 action creator 是一个创建 action 的工厂。
举个例子
const actionCreators = {
decrement: (val) => ({ type: 'DECREMENT', payload: val }),
modifyContent: () => ({
type: 'MODIFY_CONTENT',
payload: { content: 'test' }
})
}
actionCreators中就包含了俩Action Creator,为了进行状态变更时不暴露dispatch和store,那就需要做一些封装性的操作
const actions = {
decrement:(val) => store.dispatch(actionCreators.decrement(val)),
modifyContent:() => store.dispatch(actionCreators.modifyContent())
}
// actions可以export到任何地方,这样就避免传递dispatch合store了
actions.decrement(2)
actions.modifyContent()
这种封装性的操作需要统一处理下,否则每有一个Action Creator就需扩展一个actions,其实就是实现bindActionCreator
const bindActionCreator = (actionCreator, dispatch) => {
return function () {
return dispatch(actionCreator.apply(this, arguments))
}
}
const bindActionCreators = (actionCreators, dispatch) => {
// 官网支持actionCreators是单个或者由多个Action Creator组成的对象,因此加上容错
if (typeof actionCreators === 'function') {
return bindActionCreator(actionCreators, dispatch)
}
if (typeof actionCreators !== 'object' || actionCreators === null) {
throw new Error()
}
const boundActionCreator = {}
for (const key in actionCreators) {
const actionCreator = actionCreators[key]
if (typeof actionCreator === 'function') {
boundActionCreator[key] = bindActionCreator(actionCreator, dispatch)
}
}
return boundActionCreator
}
思路比较简单,本质就是提取下公共代码
至此,一个高度还原的Redux就实现了,完整代码可见demo7。当然源码中还有很多容错校验,可以参考源码进行研究学习
本文对redux的探索是层层递进的方式去进行,这个思路得益于brickspert大佬的这篇文章,“站在巨人肩膀上前行”,给大佬点个赞。自己在实现的过程中也有很多其他的见解和感受。同时自己也将redux源码整体过了一遍,学习的不仅是代码更多的是思路和想法。
本仓库是使用rollup进行打包,若想要调试某个demo需要将gulpfile中inputOptions的入口文件路径改为对应的demoX
const inputOptions = {
input: path.resolve(__dirname, './src/demoX/index.js'),
...
}