趣味性探究Redux核心源码

团队的项目中用了很久的 reactredux,一值想弄懂 redux 数据存储背后的逻辑,前段时间研究了下官方源码分享出来。这篇文章试图通过形象化的场景模拟来解读 redux 背后的设计,这种方式可能印象会更加深刻些。

redux的核心功能

redux的核心功能主要包括下面几个

  • store: 可以简单描述成一个控制中心,可以获取 state,也可以改变 state,但是改变 state 需通过间接的方式
  • reducer: 将 state 和其中变量的最新数据做一次合并返回 state
  • action: 管理着改变 state 某些数据的行为,操作是同步的
  • asyncAction: 管理着改变 state 某些数据的行为,操作是异步的,比如请求接口
  • listener: 观察者模式下,由 store 控制着 listener,当有变更消息来时就触发 listener 执行更新

项目也常用到中间件比如 thunk

  • thunk: 可以嵌套触发 action

场景转换

如果看不大懂上面的概念,没关系,或许看过下面的场景后会有一些认识的转变

我试图将上面的功能做一下场景的转变,场景是到店里吃饭,是生活中经常发生在你我身上的事情。如下我对功能做了标记

  • store: 顾客
  • thunk: 服务员
  • action: 拿纸巾、加餐具、舔茶水、摆菜品
  • asyncAction: 去后厨取菜品
  • reducer: 餐桌(包括菜品区和碗碟区)
  • listener: 算总价

顾客是整个就餐过程的核心人物,他可以吩咐服务员做各种事情,包括拿纸巾、上餐具、加茶水、上菜品等事情,我们假设,服务员每次拿完东西回来放在餐桌上后,顾客看到了会自发去算一遍桌上菜品(包括碗碟区)的总价。现在就开始点餐流程

两位顾客来到了名叫沫沫的餐厅找了一个桌位坐了下来,顾客看到餐桌上有一份纸巾和一套餐具,瞬间自发地在心里算了一笔帐。然后开始了点菜,他们点了清蒸鱼片、酸菜鱼和剁椒鱼头三道菜,于是服务员吩咐厨房开始制作,需要至少5分钟后才能做出菜品。

坐下来一小会儿,顾客A发现少了一份餐具,于是叫服务员拿份餐具,服务员取来餐具很有礼貌地放在了餐桌的碗碟区。这激发了顾客在心里算起了价格。

又过了一会儿,顾客B有点渴了,叫来服务员端两杯茶水,服务员取了两杯茶这次依然很有礼貌地放在了餐桌的碗碟区,这再次激发了顾客心里算起了价格,这就有点搞笑了,茶水是免费的,哈哈,不过这总归是条件反射嘛。

5分钟过去了,他们的三道菜品端上来了,服务员很自然地放在了餐桌的菜品区,这下顾客要好好算了,很得意地,顾客算好了。

没过多久,菜吃得差不多了,顾客A还没吃饱,于是吩咐服务员再点最后一道菜烤鱼,顺便再添加些茶水,这道菜后厨需要至少2分钟才能做出来,服务员吩咐了厨房并端来了茶水壶,给顾客加了两杯茶水。

2分钟过后,烤鱼来了,服务员摆放到了餐桌菜品区,顾客这次算了最后的总价,算完后两人开始吃起来了。

就餐的过程中两位顾客相聊甚欢,吃得差不多了准备起身结账时,发现餐桌上有一份纸巾,顾客并不需要这个纸巾,于是叫服务员拿掉了这个纸巾,并再次算起了总价,这次真的是最后的总价了。

最后顾客到前台完成了结账,满心欢喜地回家了。

看到这儿,如果能看懂,其实对 redux 的底部设计了解得差不多了。
为了将这个故事反应到 redux 的设计上,我准备将关键信息和 redux 的关键功能对应上如下。

我们大概已经知道 redux 面对一个 action 的内部处理流程了,之所以需要这么规范化复杂化的流程设计,想必就是各司其职,防止越位,最后防止 state 被恶意篡改。

开启源码解读

这篇文章就这么结束了吗,想的美,接下来就是重头戏。

对 redux 的设计有了一些认识后,我们结合它的核心源码来探究它是怎么完成上面的设计的,源码使用的是 gh-pages 版本,只提取其中的核心部分,下面是 redux 的 github 源码目录,包括 creatStorecomposecombineReducersapplyMiddleware,我们暂且略过 bindActionCreators,并引入 thunk 一起看看他们是怎么结合的。

场景转为示例

首先抛出一个具体的示例,仿真一开始的故事情节编写。为了模拟多个 reducer 模块,我们将加餐具、舔茶水、退纸巾的操作看成一个模块,针对菜品的操作看成另一个模块。

这是 reducer1,存储着餐具、茶水、纸巾的初始化数据,以及更改这些数据的操作

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
// reducer1.js

import { ADD_TABLEWARE, ADD_TEA, MINUS_NAPKIN } from '@store/actions/action1';

const initialState = {
tablewareCount: 1,
teaCount: 0,
napkinCount: 1,
}

export default (state = initialState, action = {}) {
case ADD_TABLEWARE:
return state.merge({
tablewareCount: action.tablewareCount,
});
case ADD_TEA:
return state.merge({
teaCount: action.teaCount,
});
case MINUS_NAPKIN:
return state.merge({
napkinCount: action.napkinCount,
});
default:
return state;
}

这是 action1,对应着 reducer1,标识着改变 state 某些数据的操作行为,包括加餐具、舔茶水、退纸巾,当然也可以对数据做一些处理后转为最终态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// action1.js

export const ADD_TABLEWARE = 'ADD_TABLEWARE';
export const ADD_TEA = 'ADD_TEA';
export const MINUS_NAPKIN = 'MINUS_NAPKIN';

export function addTablewareAction(count) {
return {
type: ADD_TABLEWARE,
tablewareCount: count,
};
}

export function addTeaAction(count) {
return {
type: ADD_TEA,
teaCount: count,
};
}

export const deleteNapkin = {
type: MINUS_NAPKIN,
napkinCount: count,
}

这是reducer2,存储着菜品的初始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// reducer2.js

import { PLACE_DISHS } from '@store/actions/action2';

const initialState = {
dishs: [],
}

export default (state = initialState, action = {}) {
case PLACE_DISHS:
return state.merge({
dishs: action.dishs,
});
default:
return state;
}

这是 action2,对应着 reducer2,包含针对菜品的操作

1
2
3
4
5
6
7
8
9
10
// action2.js

export const PLACE_DISHS = 'PLACE_DISHS';

export function placeDishsAction(dishs) {
return {
type: PLACE_DISHS,
dishs,
};
}

这是 asyncAction1,在这里的 action 都包含有异步操作,通常是请求接口数据,包括去后厨取菜品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// asyncAction1.js

import dish from '@api/dish';
import { addTeaAction } from '@store/actions/action1';
import { placeDishsAction } from '@store/actions/action2';

export const getDishs = () => (dispatch, getState) => {
dish.getDishAPI()
.then((res) => {
const { dishs } = res.data;
const teaCount = 2;
dispatch(placeDishsAction(dishs));
dispatch(addTeaAction(teaCount));
})
}

这里是生成控制中心 store 的地方,也就是顾客

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// store.js

import { createStore, compose, applyMiddleware, combineReducers} from 'redux';
import thunk from 'redux-thunk';
import reducer1 from './reducers/reducer1';
import reducer2 from './reducers/reducer2';

const rootReducer = combineReducers({
reducer1,
reducer2
});

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(rootReducer, composeEnhancers(
applyMiddleware(thunk),
));

export default store;

至此,我们完成了一个 redux 的基本结构,包括两个 reducer 及对应的 action,同时还有一个 asyncAction 以模拟包含异步的操作(取菜品对应请求接口这种类似的操作),最后在 store.js 中做两个 reducer 的合并,同时引入中间件 thunk 做 store 初始化。之后就是页面引入这个 store 就可以做相关的业务逻辑了,这块的会涉及到 react-redux 相关,到时会单独来讲解,在这里咱们只关注 redux 的内部实现。

createStore

接下来就是,怎么去看redux这个源码,我们顺着store的初始化来看,因为这是页面逻辑唯一引入redux的地方。可以看到有几个函数我们需要关注

  • combineReducers
  • createStore
  • composeEnhancers
  • applyMiddleware
  • thunk

其实这基本是今天要说的全部内容了。

首先看 createStore,这是 store 初始化的地方,我们剔除源码中的异常等逻辑,只提取核心逻辑来看。

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 来源 redux 中的 createStore.js,做了部分修改,剔除了replaceReducer和[$$observable]

export default function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}

if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}

return enhancer(createStore)(reducer, preloadedState)
}

let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
function getState() {
return currentState
}

function subscribe(listener) {
nextListeners.push(listener)
return function unsubscribe() {
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}

function dispatch(action) {
currentState = currentReducer(currentState, action)
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}

dispatch({ type: ActionTypes.INIT })

return {
dispatch,
subscribe,
getState,
}
}

结合 store.js 中的 createStore 来看,不考虑插件情况 composeEnhancers 即为 compose

1
const store = createStore(rootReducer, compose(applyMiddleware(thunk)));

其中

  1. rootReducer 对应 reducer
  2. compose(applyMiddleware(thunk)) 对应 preloadedState
  3. enhancer 是 undefined

createStore 最后返回的是

1
enhancer(createStore)(reducer, preloadedState)

即执行下方,其中 preloadedStateundefined,并且 createStore 被作为参数传入

1
(compose(applyMiddleware(thunk))(createStore))(reducer, preloadedState)

compose

看起来是相当复杂的,我们一步步来看,首先是看下 compose 它的作用

1
2
3
4
5
6
7
8
9
10
11
12
// 来源 redux 中的 compose.js
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}

if (funcs.length === 1) {
return funcs[0]
}

return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

代码很简洁,可以看出主要用于多个函数的合并操作,按照 funcs 的顺序从右向左做合并,其中 b 接受到参数 args 后运行的结果作为 a 的参数,a 的运行参数作为 a 左边函数的参数,这样将 funcs 遍历一遍,用一个栗子来看看

1
2
3
4
5
6
7
8
function add (a) {  
return function (b) {
return a + b
}
}
// 得到合成后的方法
const add6 = compose(add(1), add(2), add(3))
add6(10) // 16 相当于 compose(add(1), add(2), add(3))(10)

这时,10 相当于 args 被传入最右边的 add(3),即执行 add(3)(10) 后结果是 13,13 作为 add(2) 的参数即执行 add(2)(13) 结果是 15,最后执行 add(1)(15),最后的结果16

回过头来再来看看之前的案例

1
2
3
(compose(applyMiddleware(thunk))(createStore))(reducer, preloadedState)
// 就相当于
applyMiddleware(thunk)(createStore)(reducer, preloadedState)

applyMiddleware

接着,我们看下 applyMiddleware 的原理,其中 middlewares 对应 thunk,当然也可以传多个中间件,这里我们只使用了一个,args 对应着 reducerpreloadedState,终于对上号了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
`Dispatching while constructing your middleware is not allowed. ` +
`Other middleware would not be applied to this dispatch.`
)
}
let chain = []

const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

return {
...store,
dispatch
}
}
}

最后的运行结果即是我们最初通过 createStore 初始化后的 store 对象,可以看到,其中的 dispatch 被重写了,是被 middleware 即我们传入的 thunk 重写了,因为 middlewares 长度为1,因此 chain 就只有一个元素, compose 函数传入的 chain 即是

1
2
3
middleware(middlewareAPI)
// 也就是
thunk(middlewareAPI)

thunk

做了 thunk(middlewareAPI)(store.dispatch) 的执行后,得到了新的 dispatch,是时候来看看 thunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;

也是非常简洁,我们改写下,其中参数中的 dispatchgetState 分别对应 middlewareAPI 中的 dispatch 和 getState,next 是 store.dispatch,action 即是业务逻辑中通过 dispatch(action) 提交的 action,这里返回的是函数,当业务逻辑中通过 dispatch(action) 时,action 可以是对象,也可以是函数;如果是函数,默认会把 dispatch, getState, extraArgument 传入,即可以在一个 action 中嵌套 action,如案例中的 getDishs 即是这样的一个 action,而 deleteNapkin 是一个对象 action,其他的如 placeDishsAction 是没有嵌套 action 的 action,当然它也是一个函数

1
2
3
4
5
6
7
8
9
10
export default function({ dispatch, getState }) {

return (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}

return next(action);
};
}

combineReducers

最后,我们来看下遗漏的点就是把多个 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
// redux 中的 combineReducers.js

export default function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers)
const finalReducers = {}
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i]

if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
const finalReducerKeys = Object.keys(finalReducers)

return function combination(state = {}, action) {
let hasChanged = false
const nextState = {}
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)
nextState[key] = nextStateForKey
hasChanged = hasChanged || nextStateForKey !== previousStateForKey
}
return hasChanged ? nextState : state
}
}

combineReducers 中,其中 reducers 就是 reducer1 和 reducer2,通过合并后存储哈希表 finalReducers 管理如下,key 为 reducer 名,func 是对应 reducer 的处理逻辑,当通过 dispatch(action) 时,会执行 combination 逻辑,遍历并执行所有的 reducer 获取最新的 state,并和之前的 state 对比以确认返回的 state

1
2
3
4
const finalReducers = {
reducer1: func1
reducer2: func2
}

不过需要注意的一点是,在 createStore 的最后会执行 dispatch({ type: ActionTypes.INIT }),这一步是初始化 state 操作,因为一开始传递的 preloadedState 是 undefined,即 createStore 中的 currentState 是 undefined,当通过 getState 获取 state 时是空的,因此需要做一次初始化操作,初始化时执行 combination 逻辑,依然是遍历每个 reducer,每个 reducer 的第一个参数 state 默认是初始化的参数 initialState,最后在 nextState 存储的将是如下结构,即通过 getState 获取的也将是这个结构

1
2
3
4
5
6
7
8
9
10
const nextState = {
reducer1: {
tablewareCount: 1,
teaCount: 0,
dishs: [],
},
reducer2: {
napkinCount: 1,
}
}

总结

看到这里,对 redux 的内部实现是不是有了一些认识,如果还是有些模糊,不妨结合源码和示例在脑海里多运行几遍,希望能对你有所帮助!


你的鼓励我会用来换杯奶茶~😁
0%