Redux原理分析

Redux 原理分析

Redux 是一个管理全局应用状态的库,redux 可以理解为是一个上帝视角,包含了应用所需要的东西,发放给需要的组件,redux 更新了也会去通知对应的组件。

回顾 redux 的使用

学习原理之前,肯定需要熟练的应用,这样才能更好的理解原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建修改store的纯函数
function xxReducer(state = { name: "chenjiang" }, action) {
switch (action.type) {
case "add":
// todo
return state;
...
default:
return state;
}
}

// 创建Store
const store = createStore(xxReducer);

// 组件内触发更新操作
store.dispatch({type: 'add'})

几大核心概念:

Store:正如其名“仓库”,它存储了所有的状态,并且提供操作它的 API。
Action:触发一个动作,告诉 redux,我将要处理什么逻辑,但是真正的逻辑并不是它去做,而是交给 reducer。
Reducder:接收到 Action 指令,找到对应的处理逻辑,处理完成后更新 Store 状态。

以下对原理剖析。

createStore

redux 版本:v5.0.1

先从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
export function createStore(reducer, preloadedState, enhancer) {
// 可以看出enhancer“增强器”,本质是一个高阶函数,传进去一些功能方法,最后返回一个更强大的store,有点类似高阶组件
if (typeof enhancer !== "undefined") {
return enhancer(createStore)(reducer, preloadedState);
}
let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = new Map();
let nextListeners = currentListeners;
let listenerIdCounter = 0;

function ensureCanMutateNextListeners() {}

function getState() {}

function subscribe(listener) {}

function dispatch(action) {}

function replaceReducer(nextReducer) {}

// 初始化: 触发一个动作,默认更新一次
dispatch({ type: ActionTypes.INIT });

const store = {
dispatch: dispatch,
subscribe,
getState,
replaceReducer,
};
return store;
}

ensureCanMutateNextListeners 拷贝一份监听器数组

主要作用是防止监听器数组被错误修改,函数逻辑其实很简单,就是把当前currentListeners监听器数组拷贝一份给nextListeners

不经有疑问为什么要多此一举呢?主要是和 Redux 订阅和通知的机制有关系。

当我们调用 dispatch ,如果此时又调用 subscribe、unsubscribe,那么就会有意外问题,比如:

  • 在 dispatch 后,redux 会遍历监听器数组,调用监听器过程中也存在添加或者移除监听器的操作,可能本次 dispatch 中遗漏了一些监听器的调用。
  • 如果在多个地方使用同一个监听器数组的引用,对数组的修改会影响到其它地方。因此最好确保是一个独立的副本,以避免不必要的副作用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 没拷贝之前,两者的引用地址是一样的
// 因为初始化调用createStore时候 let nextListeners = currentListeners;

// 版本4.xx中
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice(); // 浅拷贝
}
}

// 版本5.0.1
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = new Map();
currentListeners.forEach((listener, key) => {
nextListeners.set(key, listener);
});
}
}

版本 5 以后为什么采用 Map 没有用原来的 slice?

因为 Map 的好处它允许我们更方便的添加、删除、以及查找监听器,并且性能比直接操作数据更高效。Map 的查找和删除操作时间复杂度是 O(1),而数组的查找和删除操作是 O(n);

getState 获取最新的状态

返回最新的状态

1
2
3
function getState() {
return currentState;
}

subscribe 订阅回调

调用订阅函数,返回一个取消订阅函数,形成闭包,那么isSubscribed就一直被保留,如果重复订阅,也就会被阻止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function subscribe(listener) {
let isSubscribed = true; // 标识当前回调已经被订阅了
ensureCanMutateNextListeners(); // 拷贝订阅者数组(map)
const listenerId = listenerIdCounter++;
nextListeners.set(listenerId, listener); // 加入新的订阅者
return function unsubscribe() {
if (!isSubscribed) {
return;
}

isSubscribed = false;
ensureCanMutateNextListeners();
nextListeners.delete(listenerId); // 移除订阅者
currentListeners = null;
};
}

dispatch 触发更新指令

接收更新指令,交给 reducer,reducer 处理完成后,通知所有订阅者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function dispatch(action) {
try {
isDispatching = true;
// 把触发更新指令交给reducer,render才是真正处理更新逻辑的
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
const listeners = (currentListeners = nextListeners);
// 通知所有订阅者
listeners.forEach((listener) => {
listener();
});
return action;
}

replaceReducer 动态替换 reducer

在运行时动态替换 store 中的 reducer

应用场景:

  • 例如不同角色,处理同一个功能,存在不同的处理逻辑,此时就需要根据登录的用户来切换 reducer 来处理不同的逻辑。
  • 动态加载 reducer:为了优化性能和加载时间,常常会将 reducer 按需加载,在需要的时候再来加载。
1
2
3
4
function replaceReducer(nextReducer) {
currentReducer = nextReducer;
dispatch({ type: ActionTypes.REPLACE });
}

combineReducers 合并多个 reducer

在整个应用中,不同的模块会有不同的处理逻辑,因此需要拆分成多个 reducer 来处理独立逻辑,但是 createStore 只能传入一个 reducer,这时就需要传之前将多个 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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 使用combineReducers
const rootReducer = combineReducers({
login: loginReducer,
home: homeReducer,
});
createStore(rootReducer);

export default function combineReducers(reducers) {
// 获取所有reducer的key
const reducerKeys = Object.keys(reducers);

const finalReducers = {};
// 从用户提供的reducer集合拷贝一份,主要是过滤掉不是函数的reducer
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === "function") {
finalReducers[key] = reducers[key];
}
}
// 拿到最终所有符合要求的reducer的key
const finalReducerKeys = Object.keys(finalReducers);

// 最终返回的是一个函数,那我们每次使用reducer时候,其实就是执行这个函数
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
// 遍历所有reducer
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
// 获取每一个reducer
const reducer = finalReducers[key];
// 旧的state状态
const previousStateForKey = state[key];
// 执行reducer过后返回新的state状态
const nextStateForKey = reducer(previousStateForKey, action);

// 将此reducer处理后的最新状态保存到map中
nextState[key] = nextStateForKey;
// 判断前后是否存在变化,也就是执行前后state引用有没有发生改变
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}

// 验证state和reudcer的数量是否匹配,要确保每个state都有reducer处理
// 之前提到过有个replaceReducer可以动态处理reducer,
// 可能在某个时间点,动态的添加或者删除reducer,如果有不匹配的情况就需要给出提示。
hasChanged = hasChanged || finalReducerKeys.length !== Object.keys(state).length;

return hasChanged ? nextState : state;
};
}

applyMiddleware 使用中间件

这个 API 主要是为了接入 Redux 生态,例如接入 redux-logger、redux-thunk 等,通俗来讲这个函数是 redux 功能的加强器(enhancer)。

整体剖析这个applyMiddleware,调用applyMiddleware返回一个函数(可以理解为加强功能的函数),我们在 createStore 源码中可以看到这一行enhancer(createStore)(reducer, preloadedState)enhancer(createStore)就可以理解为加强 createStore 功能,返回一个加强后的 createStore(Plus 版本),再次调用就等同于 createStorePlus(reducer, preloadedState)

下面再来看看细节,它是如何加强的。

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
// 看看如何使用
createStore(rootReducer, applyMiddleware(thunkMiddleware, loggerMiddleware));

// applyMiddleware源码
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
// 一开始调用原版的createStore
const store = createStore(reducer, preloadedState);
let dispatch = () => {};
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args),
};

// 将getState和dispatch两个函数传给中间件函数使用
const chain = middlewares.map((middleware) => middleware(middlewareAPI));

// 替换dispatch函数为加强版的dispatch
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch,
};
};
}

有和没有 compose 处理,有什么不同呢?

1
2
3
4
5
6
7
8
9
10
applyMiddleware(thunkMiddleware, loggerMiddleware);

// 没有compose处理
// 先执行loggerMiddleware,然后再来执行thunkMiddleware,两者并没有联系
loggerMiddleware();
thunkMiddleware();

// 有compose处理,上一个执行结果,作为下一个执行的参数(初始化值),这样多个中间件就存在联系。
// 类似管道,前面处理完了,后面在前面处理的结果基础上再接着处理。
thunkMiddleware(loggerMiddleware());

compose 将多个 middleware 产生联系

也就是上面提到,前面中间的中间件执行结果,最为后面中间件的执行基础.

先来分析,我们需要什么的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let num = 1;
const fn1 = (n) => {
return n + 1;
};
const fn2 = (n) => {
return n + 2;
};
const fn3 = (n) => {
return n + 3;
};

// 我们想要的样子, fn3的执行结果作为fn2的初始值,fn2的执行结果作为fn1的初始值,这样就产生联系。
const total = fn1(fn2(fn3(num)));

// 我们需要一个函数来处理
const total1 = compose(fn1, fn2, fn3)(num);

compose 内部是如何处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default function compose(...funcs) {
if (funcs.length === 0) {
// infer the argument type so it is usable in inference down the line
return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}

// reduce这个函数,回调函数有2个参数,第一个参数是上一次回调返回的结果
// 从逻辑来看,函数是从右往左执行的,前一次的执行结果,作为下一次参数
return funcs.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}

总结

  1. Redux 就是一个发布订阅模式,有数据更新了就通知所有订阅者。
  2. dispatch 并不是真正的去处理逻辑,而是触发更新指令,然后交给 reducer 去处理逻辑。
  3. applyMiddleware 返回的是一个加强器,类似装饰器模式,调用加强器传入 createStore,返回一个加强版 createStore,也返回了一个提供了一个加强版 dispatch 函数。
  4. 遍历监听器数组,执行回调时候也有可能发生订阅或者取消订阅,这也就是源码换种频繁使用ensureCanMutateNextListeners保证所有订阅者都能收到消息。