redux 深入探究

redux 的思想前面已经介绍过了。本文主要简单的看下,redux 的具体实现。redux 主要提供了如下几个功能:

  • 创建 store,即:createStore()。
  • 创建出来的 store 提供subscribe,dispatch,getState这些方法。
  • 将多个reducer合并为一个reducer,即:combineReducers()。
  • 应用中间件,即applyMiddleware()。

createStore 实现

简化版代码如下:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
export default function createStore(reducer, preloadedState, enhancer) {
let currentReducer = reducer // reducer
let currentState = preloadedState // 默认 state
let currentListeners = [] // 发布订阅模式队列
let nextListeners = currentListeners // 浅拷贝下这个队列
let isDispatching = false // isDispatching 标志是否正在执行dispatch
// @enhancer
if (typeof enhancer !== 'undefined') {
// 如果enhancer存在,那他必须是个function, 否则throw Error哈
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
// @1
function getState() {
// 为了确保state的正确性(获取最新的state),判断是否正在 dispatch
if (isDispatching) {
throw new Error(.../);
}
// 确定currentState是当前的state 看 -> subscribe
return currentState
}
// @2
function subscribe(listener) {
if (isDispatching) {
throw new Error(.../);
}
let isSubscribed = true;
// 如果,nextListeners 和 currentListeners 是一个引用,就拷贝 currentListeners 给 nextListeners
ensureCanMutateNextListeners()
// 添加一个订阅函数
nextListeners.push(listener)
return function unsubscribe() {
if (!isSubscribed) {
// 没有直接订阅 则 return
return
}
// 同理
if (isDispatching) {
throw new Error('')
}
// 取消订阅
isSubscribed = false
// 保存订阅快照
ensureCanMutateNextListeners()
// 找到并删除当前的listener
const index = nextListeners.indexOf(listener)
nextListeners.splice(index, 1)
}
}
// @3
function dispatch(action) {
// 各种常规判断
...
// dispatch中不可以有进行的dispatch
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
// 执行时标记为true
isDispatching = true
// 执行reducer
currentState = currentReducer(currentState, action)
} finally {
// 最终执行, isDispatching标记为false, 即完成状态
isDispatching = false
}
// 执行所有监听队列
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
// 执行每一个监听函数
listener()
}
// 返回传入的action
return action
}
// 初始化数据
dispatch({
type: ActionTypes.INIT
})
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}

getState

可以看出,createStore 将 state 通过闭包保存在了 currentState 中,通过调用 getState 返回。为了能够初始化数据,在 createStore 的最后,dispath 了一个 ActionTypes.INIT 请求。在这里 ActionTypes.INIT 实际上为一个随机字符串 @@redux/INIT${randomString()},这就保证了其不会命中任何 action,而是走默认 return state 完成数据初始化过程。

subscribe

一个简单的发布订阅模式,所有注册函数保存在 newListeners (执行的时候,会用 currentListeners) 队列中。返回一个 unsubscribe 来清除注册函数。

dispatch

直接调用 reducer 函数来进行处理,并且执行所有的 listeners。可以看见整个过程都是同步的,这也是 redux 不能在 reducer 中书写异步的原因之一(最主要的还是因为纯函数,不能涉及IO)。
这里 dispatch 对参数 action 做了检查:action 必须是一个纯对象,且必须有 type 属性。

combinReducers

combinReducers 可以将多个 reducers 合并在成为一个 reducers,从而给 createStore 使用。简单的用法如下。简单来说就是讲,各个 reducer 上的 state 统一管理在一个 key 值上,action 判断都统一放在一起。

1
2
3
4
combineReducers({
key1: reducer1,
key2: reducer2
})
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
function combineReducers(reducers) {
// 先获取传入reducers对象的所有key
const reducerKeys = Object.keys(reducers)
// 最后真正有效的reducer存在这里
const finalReducers = {}
// 下面从reducers中筛选出有效的reducer
for(let i = 0; i < reducerKeys.length; i++){
const key = reducerKeys[i]
if(typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key]
}
}
// 有效的 key
const finalReducerKeys = Object.keys(finalReducers);
let shapeAssertionError
try {
// 检查finalReducer中的reducer接受一个初始action或一个未知的action时,是否依旧能够返回有效的值。
assertReducerShape(finalReducers)
} catch (e) {
shapeAssertionError = e
}
// 返回合并后的reducer
return function combination(state= {}, action){
// 取得每个子reducer对应的state,与action一起作为参数给每个子reducer执行。
let hasChanged = false //标志state是否有变化
let nextState = {}
// 执行所有的子 reducer
for(let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i]
const reducer = finalReducers[key]
// 当前key的state值
const previousStateForKey = state[key]
// 执行reducer, 返回当前state
const nextStateForKey = reducer(previousStateForKey, action)
//存到nextState中(总的状态)
nextState[key] = nextStateForKey
// 如果子reducer不能处理该action,那么会返回previousStateForKey
hasChanged = hasChanged || previousStateForKey !== nextStateForKey
}
// 当所有状态都没改变时,我们直接返回之前的state就可以了。
return hasChanged ? nextState : state
}
}

这里我们可以看见,融合后的 reducer 当一个 action 触发时,不仅仅是该 action 对应的 reducer 执行了,其他所有的 reducer 也同时被执行了,只是 action type 不匹配,state 的值不变而已。这种空操作,可能会存在性能的浪费。(理论上大部分都不会有问题,只有节点数量多了才会有,最重要的优化是:没问题之前不做优化)
我们可以借助 redux-ignore 来指定黑白名单的方式,返回优化后的 reducer。

1
2
3
4
5
6
7
8
9
import { ignoreActions, filterActions } from 'redux-ignore';
// 黑名单写法
ignoreActions(reducer, [ARRAY_OF_ACTIONS])
ignoreActions(reducer, (action) => !action.valid)
// 白名单写法
filterActions(reducer, [ARRAY_OF_ACTIONS])
filterActions(reducer, (action) => action.valid)

原理也很简单,就是利用 actions.indexOf(action.type) >= 0 来决定 reducer 是否触发而已。

applyMiddleware 中间件

我们知道 reducer 本身是一个纯函数,纯函数要求自身不能和 IO 产生关联。这也就注定 reducer 本身是一个同步函数。同样,我们在 store.dispath 源码中看见,当一个 dispath 发出后,所有的监听回调也就同步发生了。
在实际业务中,我们的请求一般都是异步的,那么做异步请求处理就只能放在 dispatch 前来做。所以我们希望有一种通用的方案,来扩展 dispath 的功能。这就是中间件的功能。下面我们思考因该如何来做一个中间件:

封装 dispath

加入我们想要加入一个 dispath 前后记录日志的功能。我们可能会这么写

1
2
3
console.log('action 触发前');
store.dispath(action);
console.log('action 触发完毕');

为了方便复用,我们扩展其为一个函数:

1
2
3
4
5
function dispatchAndLog(store, action) {
console.log('action 触发: ', action.type);
store.dispatch(action);
console.log('action 触发完毕', store.getState());
}

重写 dispatch

现在我们可以通过调用 dispatchAndLog 来完成带有日志的 dispath。日志记录功能可以直接反映到 dispath 上面,不改变原有的用法,更直接些。通过复写 dispath 来实现扩展:

1
2
3
4
5
6
7
8
9
// 保存 dispath 副本
let next = store.dispatch;
store.diapatch = function (store, action) {
console.log('action 触发: ', action.type);
let result = next(action);
console.log('action 触发完毕', store.getState());
// 返回 action
return result;
}

现在我们随便使用 dispath 就可以实现日志的打印,而不用费事的去使用 dispatchAndLog 函数。

新的扩展

实际开发中,我们可能不仅需要日志功能,还有可能需要其他的扩展功能。比如异常上报。为此我们需要将上述操作写成两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function extendsLogFunc(store, action) {
let next = store.dispatch;
store.diapatch = function (store, action) {
console.log('action 触发: ', action.type);
let result = next(action);
console.log('action 触发完毕', store.getState());
// 返回 action
return result;
}
}
function extendsErrorPublish(store, action) {
let next = store.dispatch;
store.diapatch = function (store, action) {
try {
return next(action)
} catch(e) {
// 错误处理
}
}
}

使用两个扩展:

1
2
extendsLogFunc(store, aciton);
extendsErrorPublish(store, action);

middleware 方法

从上面,我们可以看出,其实扩展多个中间件,就是复写多次 store.dispatch 方法。每次 next 缓存的都是上一个中间件替换好的 dispatch 方法。即如下链式调用:

1
next(next(next(...args)))

即洋葱模型。我们提供这样一个封装函数,来实现这一模型。

1
2
3
4
5
6
7
8
function applyMiddleware(store, middleswares) {
middlewares = middlewares.slice();
// 翻转是为了实现从内向外逐步执行
middlewares.reverse();
middlewares.forEach(middleware =>
store.dispatch = middleware(store);
)
}

extendsLogFunc 稍作改动(extendsErrorPublish, 同理)

1
2
3
4
5
6
7
8
9
10
function extendsLogFunc(store) {
let next = store.dispatch;
return function (action) {
console.log('action 触发: ', action.type);
let result = next(action);
console.log('action 触发完毕', store.getState());
// 返回 action
return result;
}
}

就可以使用了:

1
applyMiddleware(store, [extendsLogFunc, extendsErrorPublish])

函数柯里化

我们观察改动后的 extendsLogFunc。无非就是层层准备参数,最后一起使用,这样我们很容易想到函数柯里化:

1
2
3
4
5
6
7
const extendsLogFunc = store => next => action => {
console.log('action 触发: ', action.type);
let result = next(action);
console.log('action 触发完毕', store.getState());
// 返回 action
return result;
}

为了能顺利运行,我们也需要晒微修改下 applyMiddleware,如下:

1
2
3
4
5
6
7
8
function applyMiddleware(store, middlewares) {
middlewares = middlewares.slice();
middlewares.reverse();
let dispatch = store.dispath;
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch);
)
}

我们再看,createStore 的源码,如果我们传入了第三个参数 enhancer 作为中间件,那么调用的是:

1
return enhancer(createStore)(reducer, preloadedState)

典型的柯里化调用,为此,我们最后修改下 applyMiddleware 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function applyMiddleware(middlewares) {
return (createStore) => (reducer, preloadedState) => {
const stores = createStore(reducer, preloadedState);
let dispatch = store.dispath;
middlewares = middlewares.slice();
middlewares.reverse();
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch);
)
return Object.assing({}, store, {dispath});
}
}
```
这样,我们就完成了,使用如下:
```javascript
import {createStore} from 'redux'
let store = createStore(reducers, applyMiddleware([extendsLogFunc, extendsErrorPublish]));

看下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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.`
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
// 实现洋葱模型
dispatch = compose(...chain)(store.dispatch)
// 返回增强的store
return {
...store,
dispatch
}
}
}

和我们的书写思路差不多吧,在源码里,通过 compose 方法,来实现的洋葱模型;compose 函数式编程中用来复合代码的:

1
2
3
4
5
6
7
8
9
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)))
}

在搭配 react 的时候,我们通常不会直接使用 redux 因为这样需要关注的优化点就有点多。通常情况下,我们会选择 react-redux 来完成和 react 的结合。下篇文章着重介绍下,react-redux 源码,并针对 redux 可能带来的性能问题进行说明。