Redux 技术分享

789 阅读9分钟

前言

提及 Redux 这个库,大家熟知的应该都是通过 Redux 结合 React-Redux 应用于 React 项目做全局状态管理。但 Redux 并不是非要依赖 React 才能工作,也可以结合其他库去使用。

我们可以在 Redux 中文文档 看到关于 Redux 的介绍:

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

通俗来讲:Redux 在内存中开辟了一块空间来存储 state 状态变量,并且内部拟定了一套更新 state 的规则机制(reducer),使用者可以通过 Redux 内部提供的特定方法来更新(dispatch)和获取(getState) state。

本篇将解读源码来学习 Redux 提供的属性、方法,来对 Redux 整体运行流程有一个深刻的理解。

与之相关的两篇文章可以翻阅这里:

  1. React-Redux 技术分享
  2. 从源码角度理解 React.Context

一、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 派发更新动作 actionreducer 纯函数中;
  • 匹配 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 是一个对象,提供了 dispatchsubscribegetState 三个方法,接下来一一解析每个方法的内部实现。

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 中会首先被执行一次,并将 dispatchgetState 作为参数:

// ...
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 纯函数已然满足不了我们的需求,因此我们希望能够划分模块,每个模块对应自己的 statereducer,因此就有了 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 时要注意:

  1. 在多个 reducer 下,所声明的 case 不能同名。当根据 action.type 来查找 case 时,会将所有具有相同 action.type 的 reducer 函数执行并更新状态,这容易产生意外 Bug;
  2. react-redux 内会判断 store.state 前后引用地址是否存在不同来决定重渲染,所以如果 state 需要更新,在 reducer 纯函数中就需要返回一个全新 state 对象,进而会使用 nextState

文末

本文在编写中如有不足的地方,👏欢迎读者提出宝贵意见。