Skip to content

开局一张图,带你解读 redux 源码 #9

@JS-Hao

Description

@JS-Hao

作为 react 技术栈的同学来说,redux 可谓是老常客 —— 一个主要用于状态管理的框架,经常出现在各种大大小小的项目中,其重要性不言而喻。对于如此重要的状态管理库,如果能知其然,也知其所以然(了解其内部原理),想必能给我们的工作、学习带来很大的帮助。

笔者最近在解读 redux 源码时,发现其代码非常简练,利于理解,很适合阅读。如果你有解读各类源码的想法,不妨先从它入手。在本篇文章中,笔者将尝试用一张图描绘出 redux 最为本质的运作流程,并配合每个环节的文字 + 代码描述,帮助大家更好地了解 redux,同时如果你已经开始在学习其源码,相信也能给你带来些许帮助。

另外,本文所使用的版本为 4.0.5,感兴趣的同学可在这里下载源码。

4.0.5中,其./src下的目录结构如下所示,后面的解读同样基于此结构进行:

一张图

首先我们先看看,当在外部发起一个 action,直到 state 完成更新并下发通知订阅者的这个过程中 redux 做了什么,可以用一张图来概括:

总得来说流程并不复杂:

  1. 我们使用 actionCreator 发起 action (当然 actionCreator 的使用只是一种规范约束,你可以直接 dispatch(action) 或者使用 bindActionCreators,这取决于你及你的团队风格)
  2. action 首先经过一系列中间件 middlewareXX 的处理(可以留意下这些中间件的图形结构,它比较类似 koa 的洋葱模型,它们本身是层层包裹的,后面会详细说明)
  3. 纯函数 combination 同时接受传入的 action 及当前的 state 树,即 currentState,并分发给对应的 reducer 分别计算不同的 subState
  4. 完成计算后,生成新的 state 树,即 newState,然后赋值给 currentState,并通知所有的 listeners 本次更新完成

需要注意的是,上文中的中间件不是必须的,它只是个可选项,如果没了它,整个流程会更简单:action 直接进入 combination
大体流程就这样,我们再看看每个模块的具体实现

模块解读

我们将按照上图流程,依次介绍每个模块的实现。另外,为了确保文章的简明扼要,笔者对源码进行了部分删减,建议读者配合源码一起食用,效果更佳:)

createStore.js

createStore 方法可谓是 redux 的集大成者,绝大部分模块,最终都在此函数内发挥其作用,所以我们先以全局的视角看看它内部到底干了些什么。

首先我们先忽略 createStore 内部各具体函数的实现细节:

export default function createStore(reducer, preloadedState, enhancer) {
  // 参数校验
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.");
    }
    // 后面会具体讲解此实现
    return enhancer(createStore)(reducer, preloadedState);
  }

  if (typeof reducer !== "function") {
    throw new Error("Expected the reducer to be a function.");
  }

  let currentReducer = reducer;
  let currentState = preloadedState;
  let currentListeners = [];
  let nextListeners = currentListeners;
  let isDispatching = false;

  function ensureCanMutateNextListeners() {
    // some ignored code...
  }

  function getState() {
    // some ignored code...
  }

  function subscribe(listener) {
    // some ignored code...
  }

  function dispatch(action) {
    // some ignored code...
  }

  function replaceReducer(nextReducer) {
    // some ignored code...
  }

  function observable() {
    // some ignored code...
  }

  dispatch({ type: ActionTypes.INIT });

  return {
    dispatch,
    subscribe,
    getState,
    replaceReducer,
    [$$observable]: observable,
  };
}

从函数的参数定义上,我们可知道它需要传入一个必须参数 reducer 及两个可选参数 preloadedStateenhancer,这与文档 API 保持一致。它具体做了以下几件事:

  1. 校验参数的合法性,其中的 enhancer 需要注意下,后面关于 applyMiddlewares 的实现也与它相关;
  2. 定义了一系列的内部变量,如 currentState currentReducer
  3. 定义了内部私有方法 ensureCanMutateNextListeners,主要配合 currentListenersnextListeners 维护发布-订阅的可靠性与有序性;
  4. 定义了一系列对外输出的函数,如我们常用的 dispatchsubscribegetState,及在代码分割场景下需动态 rootReducer 替换用到的 replaceReducer 和在响应式编程上(如 rxjs)可配合使用的 observable
  5. 调用内部行为 ActionTypes.INIT 通知 store 被初始化
  6. 最终输出一个集成上述函数的对象 store

从输出的对象上看也印证了上面的观点 —— createStore 返回的 store 确实是集大成者,接下来我们看看这些内部方法的具体实现(考虑篇幅关系,笔者只挑选了比较常用的几种方法进行讲解)

dispatch

function dispatch(action) {
  if (!isPlainObject(action)) {
    throw new Error(
      "Actions must be plain objects. " +
        "Use custom middleware for async actions."
    );
  }

  if (typeof action.type === "undefined") {
    throw new Error(
      'Actions may not have an undefined "type" property. ' +
        "Have you misspelled a constant?"
    );
  }

  if (isDispatching) {
    throw new Error("Reducers may not dispatch actions.");
  }

  try {
    isDispatching = true;
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }

  const listeners = (currentListeners = nextListeners);
  for (let i = 0; i < listeners.length; i++) {
    const listener = listeners[i];
    listener();
  }

  return action;
}

可以看到此函数的最核心之处在于调用纯函数 currentReducer 生成新的 currentState, 并在结束后遍历 listeners 触发订阅回调。这里需要主要下标识变量 isDispatching,在 createStore 内部各处都会出现它的身影,它充当“锁”的作用 —— 当在调用 reducer 重新计算 store 时,会锁住以下功能:

  • 不允许再次触发 dispatch
  • 不允许新增或取消订阅
  • 不允许 getState

以上行为当且仅当在非 dispatching 阶段才开放

subscribe

function subscribe(listener) {
  if (typeof listener !== "function") {
    throw new Error("Expected the listener to be a function.");
  }

  if (isDispatching) {
    throw new Error(
      "You may not call store.subscribe() while the reducer is executing. " +
        "If you would like to be notified after the store has been updated, subscribe from a " +
        "component and invoke store.getState() in the callback to access the latest state. " +
        "See https://redux.js.org/api-reference/store#subscribelistener for more details."
    );
  }

  let isSubscribed = true;

  ensureCanMutateNextListeners();
  nextListeners.push(listener);

  return function unsubscribe() {
    if (!isSubscribed) {
      return;
    }

    if (isDispatching) {
      throw new Error(
        "You may not unsubscribe from a store listener while the reducer is executing. " +
          "See https://redux.js.org/api-reference/store#subscribelistener for more details."
      );
    }

    isSubscribed = false;

    ensureCanMutateNextListeners();
    const index = nextListeners.indexOf(listener);
    nextListeners.splice(index, 1);
    currentListeners = null;
  };
}

subscribe 主要用于新增订阅者,并返回一个取消订阅的函数 unsubscribe。大家需要注意下这里的两个订阅者列表 currentListenersnextListeners,也许你会很好奇为什么 redux 需要在内部维护两个订阅者列表呢?因为 redux dispatch 完后、在遍历 listeners 并触发订阅回调时,它并不清楚订阅者中会不会存在新增或取消订阅的行为,为了保证发布-订阅的可靠性与有序性,它通过两个列表来实现“快照”功能 —— currentListeners 表示当前的订阅者列表,store 更新完毕后,redux 是遍历此订阅列表进行消息通知,而如果在此期间发生了新增或取消订阅,则会把这部分的变化更新到 nextListeners 中 —— 这意味着 nextListeners 始终存放着变动的、未来的订阅者,关于这一点的设计,在返回的 unsubscribedispatch 函数中都均有体现

PS: 在此期间 ensureCanMutateNextListeners 函数也发挥重要作用,用于确保两个订阅者列表是不同的引用,一旦两者引用相等时,则使用浅拷贝的方式再次分开两者

getState

function getState() {
  if (isDispatching) {
    throw new Error(
      "You may not call store.getState() while the reducer is executing. " +
        "The reducer has already received the state as an argument. " +
        "Pass it down from the top reducer instead of reading it from the store."
    );
  }

  return currentState;
}

getState 就非常简单了,直接返回内部的 currentState 即可

createStore 的介绍到这里基本就结束了,接下来我们再一一介绍其他相关模块。我们就从一个 action 的发起开始吧,与 action 发起相关的函数为 bindActionCreator,一起看看它的内部实现。

bindActionCreator.js

从 redux 官方文档可知,函数 bindActionCreator 的作用在于绑定 action
dispatch,这样就无须单独引入 dispatch 并手动调用,其在项目开发中的好处不言而喻:例如对于 react 展示型组件,内部则无须引入 dispatch,取而代之的是由父组件传递而来的经过 bindActionCreators 包装过后的函数,使其充分解耦。

我们来看看 redux 是怎么实现它的:

function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(this, arguments));
  };
}

export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === "function") {
    return bindActionCreator(actionCreators, dispatch);
  }

  // some ignored code here...

  const boundActionCreators = {};
  for (const key in actionCreators) {
    const actionCreator = actionCreators[key];
    if (typeof actionCreator === "function") {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators;
}

上述代码最为核心的是 bindActionCreator 函数,它借助闭包,使得返回的函数拥有访问 dispatch 的权限,以此实现绑定。由于代码过于简单,这里就不再赘述,我们继续往下看,当一个 action 发起后,首先它会经过哪个模块呢?是直接进入 reducer 重新计算 store 吗?当项目没有应用任何中间件时,确实是这样的,但一旦传入了中间件后,它首先需要经过一层层中间件的“洗礼”,因此在介绍 combineReducers 前,首先看下与中间件机制相关的函数 applyMiddleware 吧~

applyMiddleware.js

当 redux 引入中间件后,action 首先要经过的是层层包裹的中间件,关于 redux 对中间件的实现思路,其实官网上已经给出了自己的推演说明,讲解得非常精彩,有兴趣的同学可直接戳 中文文档 进行查看

import compose from "./compose";

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);

    return {
      ...store,
      dispatch,
    };
  };
}

首先 redux 引入了工具函数 compose,它是用来干什么的呢?我们先看看其代码:

export default 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)));
}

从这段代码可以看出,compose 的作用是:传入一组任意数量的函数,比如 funcA, funcBfuncC,可生成一个新的函数 (...args) => funcA(funcB(funcC(...args))),**它的含义是每个函数均以上一个函数的返回值为参数传入,并将自己计算得到的返回值作为下一个函数的参数。**对于第一次接触 compose 概念的童鞋来说可能比较绕,可多花思考体会一下。

回到代码本身,当引入了 compose 之后,下面则为 applyMiddleware 函数的实现:当我们执行 applyMiddleware(...middlewares) 时,它返回了一个诸如 (createStore) => (...args) => { ... } 的柯里化函数,等等...这是什么鬼!?从代码实现上看,它确实有点绕,因此对于后面的讲解希望大家能打起十二分精神,结合源码,多看几次

化简后的 applyMiddleware

笔者将上述代码进行了化简,同时截取了部分 createStore 的代码,结合阅读更便于理解它的工作原理:

export default function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args);

    // some ignored code...

    dispatch = compose(...chain)(store.dispatch);

    return {
      ...store,
      dispatch,
    };
  };
}

截取的部分 createStore

export default function createStore(reducer, preloadedState, enhancer) {
  if (typeof enhancer !== "undefined") {
    if (typeof enhancer !== "function") {
      throw new Error("Expected the enhancer to be a function.");
    }

    return enhancer(createStore)(reducer, preloadedState);
  }
  1;
  // some ignored code...
}

createStore 函数的第三个参数 enhancer,其实就是 applyMiddleware() 执行后返回的函数,显而易见,当存在 enhancer 时,redux 会做一层递归 —— 把 createStore 及传入的 reducerpreloadedState 分别传递给 enhancer,由它的内部完成 store 的创建工作,并返回出去供业务使用;

由于 enhancer 获得了创建并返回 store 的权利,因此可以偷偷给 store “做手脚”:比如在 applyMiddlewares 内部它篡改了 dispatch 函数 —— 它借助 compose + middlewares 对最初始的 dispatch 函数进行层层包装,以实现各种丰富功能;

看到这里,我们已经对 applyMiddlewares 的大致功能有了了解,接下来我们再看看它是如何通过包装 dispatch 以实现中间件功能的

dispatch 的层层包装

光看 applyMiddleware 的内部实现可能还无法完全理解,需要加上一个简易的中间件配合阅读,这里笔者实现了个非常简单的类似 redux-thunk 的功能 —— 赋予 redux 处理异步函数的能力:

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);

一个简易 redux-thunk 的具体实现

const myThunk = ({ getState, dispatch }) => (next) => (action) => {
  if (typeof action === "function") {
    action(dispatch, getState);
  } else {
    next(action);
  }
};

它们的执行流程如下:

  1. 首先执行 middleware(middlewareAPI) 返回格式为 next => action => {...} 的函数,函数通过闭包,可随时访问 dispatchgetState
  2. 借助上述 compose 机制,next => action => {...} 函数中的 next 为上个函数的执行结果,而自身计算的结果将作为下一个函数的参数使用,因此效果类似如下代码:
    const finalDispatch = funcA(funcB(funcC(store.dispatch)));
    store.dispatch = finalDispatch;
  3. 由于 finalDispatch 最终会触发原本的 store.dispatch,不仅机制上来说结果不变,而且中间件们还能在这期间实现自己的功能:比如处理异步函数、promise、输出日志等等;

以上代码可以得出 redux 实现中间件的核心思想在于 dispatch 函数的包装与复写 —— 通过一系列中间件的层层包装,我们最终在外边拿到的,并非是最初在 createStore 内部定义的原汁原味的 dispatch,它早已被 middleware 们“动了手脚”,也正是因为改写了 dispatch,诸如 redux-thunkredux-promise 等中间价赋予了 redux 处理异步的能力,actionCreator 的返回值得到了拓展,它不再必须为纯对象,返回函数或 promise 也是可以的

combineReducers

其实我们在使用 reducer 时,不一定非要使用 combineReducers 函数,它只是提供了组合多个子 reducer 的一种方式。关于 combineReducers 的实现,官方已经在文档上给出了大概的实现,但我们还是来看看真实的内部代码是怎样的吧:

export default function combineReducers(reducers) {
  const reducerKeys = Object.keys(reducers);
  const finalReducers = {};
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i];

    if (process.env.NODE_ENV !== "production") {
      if (typeof reducers[key] === "undefined") {
        warning(`No reducer provided for key "${key}"`);
      }
    }

    if (typeof reducers[key] === "function") {
      finalReducers[key] = reducers[key];
    }
  }
  const finalReducerKeys = Object.keys(finalReducers);

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache;
  if (process.env.NODE_ENV !== "production") {
    unexpectedKeyCache = {};
  }

  let shapeAssertionError;
  try {
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  return function combination(state = {}, action) {
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    if (process.env.NODE_ENV !== "production") {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      );
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    let hasChanged = false;
    const nextState = {};
    for (let i = 0; i < finalReducerKeys.length; i++) {
      const key = finalReducerKeys[i];
      const reducer = finalReducers[key];
      const previousStateForKey = state[key];
      const nextStateForKey = reducer(previousStateForKey, action);
      if (typeof nextStateForKey === "undefined") {
        const errorMessage = getUndefinedStateErrorMessage(key, action);
        throw new Error(errorMessage);
      }
      nextState[key] = nextStateForKey;
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length;
    return hasChanged ? nextState : state;
  };
}

笔者上面所贴的代码,其实删减了一些跟告警、校验相关的函数。combineReducers 做了以下几件事情:

  1. 校验 reducers 的合法性,包括:是否存在 undefined 的值、剔除假 reducer(即判定是否为函数),以及判定每个 reducer 能否正常处理未知 action、能否在初始化正常返回 initialState 等等;
  2. 返回了一个新函数 combination,也就是我们常说的 rootReducer
  3. 当执行 combination 后,combination 先进行参数校验,随后通过遍历 finalReducerKeys 获取每个 state 的 key 及对应的 reducer,并计算出新的 state 即 nextStateForKey
  4. 当存在 nextStateForKey !== previousStateForKey 时,则返回新的 state, 否则返
    回旧的 state (这里需要注意下,它是通过标志位 hasChanged 来判断 state 是否有更新的)

结尾

写到这里,redux 的代码就基本读完了,总的来说,如果要让笔者用一个词来形容它,那就是“精炼”——会有一种麻雀虽小五脏俱全的感觉,而且有些地方的实现也非常巧妙,还是很推荐大家有时间读一读源码,相信会非常有收获的:) 由于笔者能力有限,可能存在部分解读不当的情况,欢迎大家拍砖~

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions