前言
提及 Redux 这个库,大家熟知的应该都是通过 Redux 结合 React-Redux 应用于 React 项目做全局状态管理。但 Redux 并不是非要依赖 React 才能工作,也可以结合其他库去使用。
我们可以在 Redux 中文文档 看到关于 Redux 的介绍:
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
通俗来讲:Redux 在内存中开辟了一块空间来存储 state 状态变量,并且内部拟定了一套更新 state 的规则机制(reducer),使用者可以通过 Redux 内部提供的特定方法来更新(dispatch)和获取(getState) state。
本篇将解读源码来学习 Redux 提供的属性、方法,来对 Redux 整体运行流程有一个深刻的理解。
与之相关的两篇文章可以翻阅这里:
一、Redux 简单使用
const redux = require('redux');
// state 定义集合
const iniState = {
count: 0
}
// reducer - 纯函数,提供相关 case 来更新 state
function reducer(state = iniState, action) {
switch (action.type) {
case "INCREMENT":
return { ...state, count: state.count + 1 };
case "DECREMENT":
return { ...state, count: state.count - 1 };
default:
return state;
}
}
// store 创建 Redux 仓库
const store = redux.createStore(reducer);
// action 定义更新的动作,即对应 reducer 函数中的更新 case
const action1 = { type: "INCREMENT" };
const action2 = { type: "DECREMENT" };
// 监听 state,当状态发生改变时会执行 subscribe 中的 cb
store.subscribe(() => {
console.log('Redux 数据发生改变!');
});
// dispatch 触发 action 更新动作,进而运转到 reducer 中寻找对应 case 来更新 state
store.dispatch(action1);
store.dispatch(action2);
store.dispatch(action1);
// getState 获取 store 中的 state
console.log(store.getState());
在上面的用例中,我们重点关注:
- 定义 Redux store 中的状态集合:
state; - 定义更新 state 的纯函数:
reducer; - 创建一个 Redux store,并接收 reducer 作为状态更新函数:
createStore; - 定义更改状态的动作类型,可以是一个对象,也可以是一个函数,:
action; - 触发更新动作的方法:
dispatch; - 注册状态变化的监听事件:
subscribe; - 获取 Redux store 中的状态:
getState。
在上面用例可以看出 Redux 的更新流程:
- 通过
dispatch派发更新动作action到reducer纯函数中; - 匹配 reducer 函数中对应的 case,进而更新状态
state。
二、源码解读
1、createStore
Redux 通过 createStore 来创建状态容器,该方法接收一个纯函数 reducer 作为必传参数,还会接收一个中间件作为可传参数,来扩展一些特定的功能,最后返回一个 store 实例对象。
常见的使用示例如下:
const { createStore, applyMiddleware } = require('redux');
// 中间件在这里可以先不用深究,后面结合 compose 方法去理解。
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action); // next 是 store.dispatch(原版的)
};
const store = createStore(reducer, applyMiddleware(thunk));
它的源码实现如下:
function createStore(reducer, preloadedState, enhancer) {
// 1、参数转换:当 createStore 第二参数是函数,并且没有传第三参数时,会将第二参数作为 enhancer 中间件逻辑去处理
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
// 2-1、当使用中间件时,会调用 enhancer(就是 applyMiddleware) 执行中间件并返回 store
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
// 2-2、当不使用中间时,会创建一个 store 对象
let currentReducer = reducer;
let currentState = preloadedState;
let listeners = [];
let isDispatching = false;
function getState() {
// ...
}
function subscribe(listener) {
// ...
}
function dispatch(action) {
// ...
}
const store = {
dispatch,
subscribe,
getState,
};
return store;
}
在 createStore 中划分为两种场景:一种是使用中间件的场景,一种是没有使用中间件的场景,这里我们先看最简单的一种情况:没有使用中间件的场景。
从上面源码我们得知:store 是一个对象,提供了 dispatch、subscribe、getState 三个方法,接下来一一解析每个方法的内部实现。
2、getState
getState 做的事情非常简单明确:返回 store 中的 state 集合。getState 利用 JS 闭包获取 createStore 执行时创建的 state 变量。
function createStore(reducer, preloadedState, enhancer) {
// ...
let currentState = preloadedState;
function getState() {
return currentState;
}
// ...
}
3、subscribe
subscribe 用于注册和销毁监听回调,当 state 状态更新后,会触发监听回调的执行。它接收一个函数作为参数,并且会返回一个新的函数,用作销毁注册的监听函数。
function createStore(reducer, preloadedState, enhancer) {
// ...
let listeners = [];
function subscribe(listener) {
// ... 省略判断 listener 不是一个函数时 throw Error
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
listeners.splice(index, 1);
}
}
// ...
}
4、dispatch
dispatch 用于派发一个 action 更新动作类型,执行传递给 store 的 reducer 纯函数中来匹配 case 更新 state 状态。
function createStore(reducer, preloadedState, enhancer) {
// ...
let currentReducer = reducer;
let currentState = preloadedState;
let listeners = [];
let isDispatching = false;
function dispatch(action) {
// ... 省略判断 action 不是一个对象时 throw Error
// ... 省略判断 action.type 不存在时 throw Error
// 执行纯函数 reducer 更新 state
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
// state 更新后,通知监听函数
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}
// ...
}
从源代码中可以看出,只要执行一次 dispatch,不论 state 是否有发生改变,listener 监听回调都会被执行。
5、applyMiddleware
接下来我们从源码角度看使用中间件的场景。
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action); // next 是 store.dispatch(原版的)
};
const store = redux.createStore(reducer, redux.applyMiddleware(thunk));
当我们将 applyMiddleware 执行结果作为 createStore 的第二参数时,会进入中间件的处理逻辑:
function createStore(reducer, preloadedState, enhancer) {
// 参数转换
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
// 使用中间件的场景
if (typeof enhancer !== 'undefined') {
return enhancer(createStore)(reducer, preloadedState);
}
// ...
}
applyMiddleware 作为柯里化函数被调用,并且将 createStroe 和 reducer 作为柯里化函数参数,它的源码实现如下:
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
// 1、调用 createStore 创建 store 对象
const store = createStore(reducer, preloadedState);
// 2、定义新的 dispatch,中间件的本质就是改造 dispatch,返回一个功能更强大的 dispatch
let dispatch = () => {};
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
// 每个中间件也都遵循函数柯里化原则,先将 getState 和 dispatch 作为参数执行
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 通过 compose 执行中间件,返回中间件增强后的 dispatch
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
}
}
}
在 applyMiddleware 中做的事情也是将 reducer 作为 createStore 的参数去执行并得到 store,唯一区别在于:调用 compose 来组织中间件,并返回一个改造、增强后的 dispatch 方法。
compose 源码实现如下:
function compose(...funcs) {
if (funcs.length === 0) {
// 如果没有中间件,则返回一个函数,此时 arg 就是 store.dispatch,继续使用原始 dispatch 方法
return (arg) => arg;
}
if (funcs.length === 1) {
// 只有一个中间件,则返回该中间件进行改造 dispatch
return funcs[0];
}
// 多种中间件的情况:
// 简单理解就是将 compose(fn1, fn2, fn3)(...args) 转换成了:fn1(fn2(fn3(...args)))
// 它是一种高阶聚合函数,相当于把 fn3 先执行,然后把结果传给 fn2 再执行,再把结果交给 fn1 去执行。
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
在 compose 这个方法中,难点也是核心就在于处理多个中间件时,这里也看到了中间件的执行顺序:
后面的中间件先执行,执行完返回的结果(结果都是一个被此中间件改造/增强后的新的 dispatch 方法)作为下一个中间件的参数,继续被改造/增强。
经过中间件的改造后,我们后续所使用的 dispatch 已然是被中间件改造/增强过的 dispatch 了,其他的功能都没有变化,所以到这里我们明确了一个概念:Redux 的中间件是用来改造/增强 dispatch 方法的。
下面我们根据 redux-thunk 这个中间件来进一步分析 dispatch 被改造的过程。
6、redux-thunk
上面我们在介绍 applyMiddleware 时贴上了 redux-thunk 源码核心实现,代码如下:
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action); // next 是 store.dispatch(原版的)
};
首先 thunk 中间件在 applyMiddleware 中会首先被执行一次,并将 dispatch 和 getState 作为参数:
// ...
let dispatch = () => {};
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args)
}
// 每个中间件也都遵循函数柯里化原则,先将 getState 和 dispatch 作为参数执行
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// ...
注意:此时传递的 dispatch 只是一个空函数,还没有被改造。接着调用 compose 去执行中间件来改造 dispatch。由于我们这里只有一个 thunk 中间件,所以在 compose 中直接使用该中间件进行改造:
// ...
dispatch = compose(...chain)(store.dispatch);
// ...
上面代码调用 thunks 中间件,将 store.dispatch(原始的 dispatch)做为参数传入,此时返回的结果就是新的、改造后的 dispatch,对应代码如下:
const thunk = ... (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
};
从上面代码分析:
next对应的就是store.dispatch,此柯里化函数执行后返回的结果:(action) => { ... }就是改造后的新的dispatch函数;- 在
新的 dispatch函数中,允许action值是一个函数,调用action函数并且会将改造后的dispatch再次传入; - 在
action函数内部调用 dispatch 时,就必须要保证新的 action是一个对象,如果还是函数,则会重复执行上面工作; - 因为只有是具有
type属性的对象时,才会执行return next(action),从而调用原始的store.dispatch来进入reducer纯函数中更新状态。
7、combineReducers
当我们项目比较庞大时,一个 reducer 纯函数已然满足不了我们的需求,因此我们希望能够划分模块,每个模块对应自己的 state 和 reducer,因此就有了 combineReducers 的出现,我们先看一下它的用法:
import { combineReducers } from 'redux';
function reducer1(state = {}, action) {
// ...
}
function reducer2(state = {}, action) {
// ...
}
const reducer = combineReducers({
reducer1,
reducer2
});
let store = createStore(reducer);
combineReducers 接收一个 reducer 对象集合,返回一个新的 reducer 来管理各个 子模块 reducer,它的源码实现如下:
function combineReducers(reducers) {
const reducerKeys = Object.keys(reducers);
// 返回的一个新的 reducer 纯函数
return function combination(state, action) {
let hasChanged = false; // 记录本次更新,state 是否有发生改变
const nextState = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
const reducer = reducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action); // 执行 reducer
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
hasChanged = hasChanged || reducerKeys.length !== Object.keys(state).length;
return hasChanged ? nextState : state;
}
}
combineReducers 会返回一个新的 reducer 函数。它会遍历每一个 子模块 reducer,将子模块对应的 state 和本次更新动作 action 作为 子模块 reducer 纯函数的参数来更新 state。
所以在使用 combineReducers 时要注意:
- 在多个
reducer下,所声明的case不能同名。当根据action.type来查找case时,会将所有具有相同action.type的 reducer 函数执行并更新状态,这容易产生意外 Bug; react-redux内会判断 store.state 前后引用地址是否存在不同来决定重渲染,所以如果 state 需要更新,在reducer纯函数中就需要返回一个全新state对象,进而会使用nextState。
文末
本文在编写中如有不足的地方,👏欢迎读者提出宝贵意见。