上篇文章中阐释了我对 Redux 架构及其复杂性的看法,提到了 Redux 本质是一个非常简单易懂的状态管理架构,本文将解析 Redux 的源码,并从零实现一个带有中间件系统的 Redux。
注:
原始链接: https://www.404forest.com/2017/09/13/modern-web-development-tech-analysis-redux-with-its-middleware/
文章备份: https://github.com/jin5354/404forest/issues/64
redux 源码: https://github.com/vuejs/vue/tree/dev/src/core/observer
本文实现的 redux(附注释和 100% 测试):https://github.com/jin5354/leaf-store
1. 设计一个 Redux
首先,我们抽出一个典型的 Redux 用法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| const initialState = { counter: 0, } const reducer = (state = initialState, action) => { switch(action.type) { case('ADD_COUNTER'): { return Object.assign({}, state, { counter: state.counter + 1 }) } default: { return state } } } const store = createStore(reducer) store.dispatch({ type: 'ADD_COUNTER' }) console.log(store.getState().counter)
|
上面是一个 Redux 的极简用例。我们看到 Redux 主要的几个功能点如下:
createStore
根据 reducer
创建 store
dispatch
派发 action
,执行 reducer
进行数据修改与更新
getState
获取当前 state
2. 实现 createStore、dispatch 和 getState
调用 createStore
应该传入一个 reducer
,返回一个 store
对象,其包含两个方法:dispatch
和 getState
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function createStore(reducer) { let state const getState = () => { return state } const dispatch = (action) => {...} return { getState, dispatch } }
|
调用 dispatch 进行数据修改时会传入 action,我们需要执行 reducer 拿到新状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function createStore(reducer) { let state const getState = () => { return state } const dispatch = (action) => { state = reducer(state, action) } return { getState, dispatch } }
|
现在 getState
和 dispatch
就可以正常工作了。不过,在 createStore
后,我们调用 getState
返回的是 undefined
。我们需要初始化 state
为初始 state
。发一个空的 disptach,获得默认 state。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function createStore(reducer) { let state const getState = () => { return state } const dispatch = (action) => { state = reducer(state, action) } dispatch({}) return { getState, dispatch } }
|
现在我们就得到了一个不过 20 行的极简版的 Redux,其已经能满足第一节中的使用需求了。
在线示例
3. 功能增强
Redux 的源码包括以下几个文件:
![Redux-1](/imgs/blog/redux-1.png)
其中 createStore.js
实现的即是 createStore
函数,其核心即为上一节的 20 行代码。我们参照 Redux,为我们的极简版本加功能。
3.1 subscribe
在 Redux 中,我们可以使用 store.subscribe(callback)
来注册监听函数,监听函数将在每次 dispatch
之后执行。我们来添加一个 subscribe 函数。这里明显要使用发布-订阅模式,维护一个 listeners
数组,其中存储全部的监听函数。执行 subscribe
返回一个 unsubscribe
函数,执行 unsubscribe
即可解绑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| function createStore(reducer) { let state const listeners = [] const getState = () => { ... } const subscribe = (listener) => { listeners.push(listener) return function unsubscribe() { const index = listeners.indexOf(listener) listeners.splice(index, 1) } } const dispatch = (action) => { state = reducer(state, action) listeners.forEach(listener => { listener() }) } dispatch({}) return { getState, dispatch, subscribe } }
|
目前 Redux
中 subscribe
就是这样实现的,不过这里有个小问题:使用 indexOf(listener)
来查找数组中 listener
的位置时,如果 listener
有多个重复的,那么只会返回第一个——也就是说如果你多次 subscribe
了一个函数,那么无论你执行哪一个 unsubscribe
,删掉的都是第一个 listener
。看看例子:
在线示例
我顺便提了个 PR,维护者认为这是极小概率事件,就先不处理了。要修复这个缺陷也好办,为每个 listener
加一个 unique ID 即可区分。
3.2 combineReducers
大型项目往往是多人开发的,多个人同时修改一个 reducer
极易造成冲突,我们希望 reducer
可以是模块化的,每个人维护一个模块,最终可以通过一个方法组装成 root reducer
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const initialStateA = { counterA: 0, } const reducerA = (state = initialStateA, action) => { ... } const initialStateB = { counterB: 10, } const reducerB = (state = initialStateB, action) => { ... } const store = createStore(combineReducers({ reducerA, reducerB })) console.log(store.getState().reducerA.counterA) console.log(store.getState().reducerB.counterB)
|
观察可知,该方法应接收一个包含多个 reducer
的对象(数组也可以,对象可以通过 key 来重命名 reducer
),返回一个组装后的 reducer
。
根 reducer
将子 reducer
的 state 以 key 为属性挂在根 reducer
的 state 上,每次接受 action 时,按 key 来获取每个子 reducer
的状态,并将产生的新状态进行替换。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| * [combineReducers 组合多个 reducer] * @param {[object]} reducers * @return {[type]} */ function combineReducers(reducers) { const reducerKeys = Object.keys(reducers) return function combination(state = {}, action) { let newState = {} let hasChanged = false reducerKeys.forEach(key => { let oldKeyState = state[key] let newKeyState = reducers[key](state[key], action) newState[key] = newKeyState hasChanged = hasChanged || newKeyState !== oldKeyState }) return hasChanged ? newState : state } }
|
4.实现中间件系统
如果你使用过 Redux,你一定知道若要在 Redux 流程内处理异步操作,必须借助中间件 Redux-thunk
。Redux 自身无法处理异步操作。dispatch
派发的 action
默认只能是一个 plain Object
。
使用 Redux-thunk
中间件后,dispatch
方法就可以接收一个函数作为参数,在函数中我们就可以实现更多的功能。Redux 是如何设计的中间件系统,使得第三方中间件可以扩展原生 dispatch
?
4.1 以 monkeypatch 举例
假设我们要为 dispatch
加一个 log 功能。能够把 dispatch
后的 state
打印出来。而使用时还是直接调用 store.dispatch
。我们可以直接对 dispatch
进行魔改。
1 2 3 4 5 6 7 8 9 10 11
| let next = store.dispatch store.dispatch = action => { next(action) console.log('dispatch 完毕,state 为:', store.getState()) }
|
简单画个图:
![Redux-2](/imgs/blog/redux-2.png)
如果要再加一个功能咋办?如法炮制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| let next = store.dispatch store.dispatch = action => { next(action) console.log('dispatch 完毕,state 为:', store.getState()) } let next2 = store.dispatch store.dispatch = action => { next2(action) }
|
这样做就实现了多个中间件的串联,每次我们调用 store.dispatch
时,实际上是这样执行的:
![Redux-3](/imgs/blog/redux-3.png)
Redux 中中间件的执行原理就是这样,但 Redux 中调整了中间件写法。我们的这个简陋版中间件系统要求开发者和用户这么使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function someMiddleware(store) { let next = store.dispatch store.dispatch = action => { next(action) } } const store = createStore(reducer) someMiddleware1(store) someMiddleware2(store) ...
|
4.2 优化 monkeypatch
上文的写法有以下几个缺点:
缺点1:用户用起来不方便,添加中间件行为明显集成在 createStore
中更方便:
1
| const store = createStore(reducer, applyMiddleware(someMiddleware1, someMiddleware2))
|
缺点2:中间件开发者权力过大,可以任意操纵 store
。Redux 只允许中间件对 dispatch
功能进行扩展,需要对中间件进行可访问性限制。
缺点3:多中间件条件下,每个中间件中拿到的 store
只是环节中的中间产物,无法拿到最终 store
。若要在中间件中派发新的 dispatch
,我们期望
使用最终 store
的 dispatch
进行派发,这样才能保证所有 dispatch
行为是一致的。
我们来看 Redux 是如何解决这三个问题的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| function createStore(reducer, enhancer) { if(typeof enhancer !== 'undefined') { return enhancer(createStore)(reducer) } let state let listeners = [] const getState = () => {...} const subscribe = (listener) => {...} const dispatch = (action) => {...} dispatch({}) return { getState, dispatch, subscribe } }
|
这样,执行 createStore(reducer, applyMiddleware(middlewares...))
即为执行 applyMiddleware(middlewares...)(createStore)(reducer)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| function applyMiddleware(...middlewares) { return (createStore) => (reducer) => { const store = createStore(reducer) let dispatch = store.dispatch const storeWithLimitedAPI = { getState: store.getState(), dispatch: (...args) => dispatch(...args) } middlewares.reverse().forEach(middleware => { dispatch = middleware(storeWithLimitedAPI)(dispatch) }) return { ...store, dispatch } } }
|
通过调整 createStore 的 API 修复了缺点1。控制参数传入,只给 middleware 传进 dispatch 和 storeWithLimitedAPI,限制了 middleware 的操作范围。storeWithLimitedAPI 中提供了最终 store 的 dispatch,使得从中间件内部派发新 disptatch 成为可能,这样缺点2和3也解决了。
applyMiddleware 中是这样调用 middleware 的:
1
| dispatch = middleware(storeWithLimitedAPI)(dispatch)
|
这就要求中间件的形式是:一个函数,接收两个参数,返回新 dispatch。我们拿 redux-thunk 的源码来看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function createThunkMiddleware(extraArgument) { return ({ dispatch, getState }) => next => action => { if (typeof action === 'function') { return action(dispatch, getState, extraArgument); } return next(action); }; } const thunk = createThunkMiddleware(); thunk.withExtraArgument = createThunkMiddleware; export default thunk;
|
中间件的执行原理可见下图。
![Redux-4](/imgs/blog/redux-4.png)
4.3 compose
初看 redux 源码时,见到 compose 可能会觉得懵逼,实际上只是个语法糖。由于多中间件时 dispatch 会被反复替换,所以会写出这样的代码:
1 2 3
| dispatch = middlewareC(middlewareB(middlewareA(dispatch)))
|
这样写不美观,中间件越多看起来越乱,所以引入 compose 函数用来整理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| dispatch = compose(middlewareA, middlewareB, middlewareC)(dispatch) function compose(...funcs) { if(funcs.length === 0) { return arg => arg } if(funcs.length === 1) { return funcs[0] } return funcs.reduce((a, b) => { return function(...args) { return a(b(...args)) } }) }
|
5.参考资料
- React.js 小书
- Redux从设计到源码
- Async Actions
- Middleware
- redux-thunk/src/index.js