前言
提及 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
。
文末
本文在编写中如有不足的地方,👏欢迎读者提出宝贵意见。