{"id":16635051,"url":"https://github.com/ruochuan12/redux-analysis","last_synced_at":"2025-08-22T18:05:00.409Z","repository":{"id":41590967,"uuid":"245054214","full_name":"ruochuan12/redux-analysis","owner":"ruochuan12","description":"《学习源码整体架构系列》多篇之redux源码，前端面试高频源码，微信搜索「若川视野」关注我，长期交流学习~","archived":false,"fork":false,"pushed_at":"2023-01-04T16:25:28.000Z","size":11631,"stargazers_count":40,"open_issues_count":46,"forks_count":8,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-27T14:57:25.474Z","etag":null,"topics":["react","react-redux","redux"],"latest_commit_sha":null,"homepage":"https://juejin.im/post/5ee63b7d51882542fc6265ad","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ruochuan12.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-03-05T02:51:48.000Z","updated_at":"2024-03-22T07:55:52.000Z","dependencies_parsed_at":"2023-02-02T18:47:18.816Z","dependency_job_id":null,"html_url":"https://github.com/ruochuan12/redux-analysis","commit_stats":null,"previous_names":["ruochuan12/redux-analysis"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fredux-analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fredux-analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fredux-analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ruochuan12%2Fredux-analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ruochuan12","download_url":"https://codeload.github.com/ruochuan12/redux-analysis/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243830953,"owners_count":20354854,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["react","react-redux","redux"],"created_at":"2024-10-12T05:49:51.839Z","updated_at":"2025-03-16T22:31:08.359Z","avatar_url":"https://github.com/ruochuan12.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 学习 redux 源码整体架构，深入理解 redux 及其中间件原理\n\n## 1. 前言\n\n\u003e你好，我是[若川](https://lxchuan12.gitee.io)，微信搜索[「若川视野」](https://mp.weixin.qq.com/s/c3hFML3XN9KCUetDOZd-DQ)关注我，专注前端技术分享。欢迎加我微信`ruochuan12`，加群交流学习。\n\n\u003e你好，我是[若川](http://lxchuan12.cn/)。这是`学习源码整体架构系列`第八篇。整体架构这词语好像有点大，姑且就算是源码整体结构吧，主要就是学习是代码整体结构，不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。\n\n\u003e**要是有人说到怎么读源码，正在读文章的你能推荐我的源码系列文章，那真是太好了**。\n\n`学习源码整体架构系列`文章如下：\n\u003e1.[学习 jQuery 源码整体架构，打造属于自己的 js 类库](https://juejin.im/post/5d39d2cbf265da1bc23fbd42)\u003cbr\u003e\n\u003e2.[学习 underscore 源码整体架构，打造属于自己的函数式编程类库](https://juejin.im/post/5d4bf94de51d453bb13b65dc)\u003cbr\u003e\n\u003e3.[学习 lodash 源码整体架构，打造属于自己的函数式编程类库](https://juejin.im/post/5d767e1d6fb9a06b032025ea)\u003cbr\u003e\n\u003e4.[学习 sentry 源码整体架构，打造属于自己的前端异常监控SDK](https://juejin.im/post/5dba5a39e51d452a2378348a)\u003cbr\u003e\n\u003e5.[学习 vuex 源码整体架构，打造属于自己的状态管理库](https://juejin.im/post/5dd4e61a6fb9a05a5c010af0)\u003cbr\u003e\n\u003e6.[学习 axios 源码整体架构，打造属于自己的请求库](https://juejin.im/post/5df349b5518825123751ba66)\u003cbr\u003e\n\u003e7.[学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理](https://juejin.im/post/5e69925cf265da571e262fe6)\u003cbr\u003e\n\u003e8.[学习 redux 源码整体架构，深入理解 redux 及其中间件原理](https://juejin.im/post/5ee63b7d51882542fc6265ad)\n\n感兴趣的读者可以点击阅读。\u003cbr\u003e\n其他源码计划中的有：[`express`](https://github.com/lxchuan12/express-analysis)、[`vue-rotuer`](https://github.com/lxchuan12/vue-router-analysis)、[`react-redux`](https://github.com/lxchuan12/react-redux-analysis) 等源码，不知何时能写完（哭泣），欢迎持续关注我（若川）。\n\n源码类文章，一般阅读量不高。已经有能力看懂的，自己就看了。不想看，不敢看的就不会去看源码。\u003cbr\u003e\n所以我的文章，尽量写得让想看源码又不知道怎么看的读者能看懂。\n\n阅读本文你将学到：\n\n\u003e1. `git subtree` 管理子仓库\n\u003e2. 如何学习 `redux` 源码\n\u003e3. `redux` 中间件原理\n\u003e4. `redux` 各个`API`的实现\n\u003e5. `vuex` 和 `redux`  的对比\n\u003e6. 等等\n\n### 1.1 本文阅读最佳方式\n\n把我的`redux`源码仓库 `git clone https://github.com/lxchuan12/redux-analysis.git`克隆下来，顺便`star`一下[我的redux源码学习仓库](https://github.com/lxchuan12/redux-analysis)^_^。**跟着文章节奏调试和示例代码调试，用`chrome`动手调试印象更加深刻**。文章长段代码不用细看，可以调试时再细看。看这类源码文章百遍，可能不如自己多调试几遍。也欢迎加我微信交流`ruochuan12`。\n\n## 2. git subtree 管理子仓库\n\n写了很多源码文章，`vuex`、`axios`、`koa`等都是使用新的仓库克隆一份源码在自己仓库中。\n虽然电脑可以拉取最新代码，看到原作者的git信息。但上传到`github`后。读者却看不到原仓库作者的`git`信息了。于是我找到了`git submodules` 方案，但并不是很适合。再后来发现了`git subtree`。\n\n简单说下 `npm package`和`git subtree`的区别。\n`npm package`是单向的。`git subtree`则是双向的。\n\n具体可以查看这篇文章[@德来（原有赞大佬）：用 Git Subtree 在多个 Git 项目间双向同步子项目，附简明使用手册](https://segmentfault.com/a/1190000003969060)\n\n学会了`git subtree`后，我新建了`redux-analysis`项目后，把`redux`源码`4.x`（截止至2020年06月13日，`4.x`分支最新版本是`4.0.5`，`master`分支是`ts`，文章中暂不想让一些不熟悉`ts`的读者看不懂）分支克隆到了我的项目里的一个子项目，得以保留`git`信息。\n\n对应命令则是：\n\n```bash\ngit subtree add --prefix=redux https://github.com/reduxjs/redux.git 4.x\n```\n\n## 3. 调试 redux 源码准备工作\n\n之前，我在知乎回答了一个问题[若川：一年内的前端看不懂前端框架源码怎么办？](https://www.zhihu.com/question/350289336/answer/910970733)\n推荐了一些资料，阅读量还不错，大家有兴趣可以看看。主要有四点：\u003cbr\u003e\n\u003e1.借助调试\u003cbr\u003e\n\u003e2.搜索查阅相关高赞文章\u003cbr\u003e\n\u003e3.把不懂的地方记录下来，查阅相关文档\u003cbr\u003e\n\u003e4.总结\u003cbr\u003e\n\n看源码调试很重要，所以我的每篇源码文章都详细描述（也许有人看来是比较啰嗦...）如何调试源码。\n\n\u003e**断点调试要领：**\u003cbr\u003e\n**赋值语句可以一步按`F10`跳过，看返回值即可，后续详细再看。**\u003cbr\u003e\n**函数执行需要断点按`F11`跟着看，也可以结合注释和上下文倒推这个函数做了什么。**\u003cbr\u003e\n**有些不需要细看的，直接按`F8`走向下一个断点**\u003cbr\u003e\n**刷新重新调试按`F5`**\u003cbr\u003e\n\n调试源码前，先简单看看 `redux` 的工作流程，有个大概印象。\n\n![redux 工作流程](./images/redux-workflow.png)\n\n### 3.1 rollup 生成 sourcemap 便于调试\n\n修改`rollup.config.js`文件，`output`输出的配置生成`sourcemap`。\n\n```js\n// redux/rollup.config.js 有些省略\nconst sourcemap = {\n  sourcemap: true,\n};\n\noutput: {\n    // ...\n    ...sourcemap,\n}\n```\n\n安装依赖\n\n```bash\ngit clone http://github.com/lxchuan12/redux-analysis.git\ncd redux-analysi/redux\nnpm i\nnpm run build\n# 编译结束后会生成 sourcemap .map格式的文件到 dist、es、lib 目录下。\n```\n\n仔细看看`redux/examples`目录和`redux/README`。\n\n这时我在根路径下，新建文件夹`examples`，把原生`js`写的计数器`redux/examples/counter-vanilla/index.html`，复制到`examples/index.html`。同时把打包后的包含`sourcemap`的`redux/dist`目录，复制到`examples/dist`目录。\n\n修改`index.html`的`script`的`redux.js`文件为`dist中的路径`。\n\n\u003e为了便于区分和调试后续`html`文件，我把`index.html`重命名为`index.1.redux.getState.dispatch.html`。\n\n```bash\n# redux-analysis 根目录\n# 安装启动服务的npm包\nnpm i -g http-server\ncd examples\nhs -p 5000\n```\n\n就可以开心的调试啦。可以直接克隆我的项目`git clone http://github.com/lxchuan12/redux-analysis.git`。本地调试，动手实践，容易消化吸收。\n\n## 4. 通过调试计数器例子的学习 redux 源码\n\n接着我们来看`examples/index.1.redux.getState.dispatch.html`文件。先看`html`部分。只是写了几个 `button`，比较简单。\n\n```html\n\u003cdiv\u003e\n    \u003cp\u003e\n    Clicked: \u003cspan id=\"value\"\u003e0\u003c/span\u003e times\n    \u003cbutton id=\"increment\"\u003e+\u003c/button\u003e\n    \u003cbutton id=\"decrement\"\u003e-\u003c/button\u003e\n    \u003cbutton id=\"incrementIfOdd\"\u003eIncrement if odd\u003c/button\u003e\n    \u003cbutton id=\"incrementAsync\"\u003eIncrement async\u003c/button\u003e\n    \u003c/p\u003e\n\u003c/div\u003e\n```\n\n`js部分`，也比较简单。声明了一个`counter`函数，传递给`Redux.createStore(counter)`，得到结果`store`，而`store`是个对象。`render`方法渲染数字到页面。用`store.subscribe(render)`订阅的`render`方法。还有`store.dispatch({type: 'INCREMENT' })`方法，调用`store.dispatch`时会触发`render`方法。这样就实现了一个计数器。\n\n```js\nfunction counter(state, action) {\n    if (typeof state === 'undefined') {\n        return 0\n    }\n\n    switch (action.type) {\n        case 'INCREMENT':\n        return state + 1\n        case 'DECREMENT':\n        return state - 1\n        default:\n        return state\n    }\n}\n\nvar store = Redux.createStore(counter)\nvar valueEl = document.getElementById('value')\n\nfunction render() {\n    valueEl.innerHTML = store.getState().toString()\n}\nrender()\nstore.subscribe(render)\n\ndocument.getElementById('increment')\n.addEventListener('click', function () {\n    store.dispatch({ type: 'INCREMENT' })\n})\n\n// 省略部分暂时无效代码...\n```\n\n思考：看了这段代码，你会在哪打断点来调试呢。\n\n```js\n// 四处可以断点来看\n// 1.\nvar store = Redux.createStore(counter)\n// 2.\nfunction render() {\nvalueEl.innerHTML = store.getState().toString()\n}\nrender()\n// 3.\nstore.subscribe(render)\n// 4.\nstore.dispatch({ type: 'INCREMENT' })\n```\n\n![redux debugger图](./images/redux-debugger.png)\n\n图中的右边`Scope`，有时需要关注下，会显示闭包、全局环境、当前环境等变量，还可以显示函数等具体代码位置，能帮助自己理解代码。\n\n断点调试，按`F5`刷新页面后，按`F8`，把鼠标放在`Redux`和`store`上。\n\n可以看到`Redux`上有好几个方法。分别是：\n- __DO_NOT_USE__ActionTypes: {INIT: \"@@redux/INITu.v.d.u.6.r\", REPLACE: \"@@redux/REPLACEg.u.u.7.c\", PROBE_UNKNOWN_ACTION: ƒ}\n- applyMiddleware: ƒ applyMiddleware() 函数是一个增强器，组合多个中间件，最终增强`store.dispatch`函数，`dispatch`时，可以串联执行所有中间件。\n- bindActionCreators: ƒ bindActionCreators(actionCreators, dispatch) 生成actions，主要用于其他库，比如`react-redux`。\n- combineReducers: ƒ combineReducers(reducers) 组合多个`reducers`，返回一个总的`reducer`函数。\n- compose: ƒ compose() 组合多个函数，从右到左，比如：compose(f, g, h) 最终得到这个结果 (...args) =\u003e f(g(h(...args))).\n- createStore: ƒ createStore(reducer, preloadedState, enhancer) 生成 `store` 对象\n\n再看`store`也有几个方法。分别是：\n\n- dispatch: ƒ dispatch(action)  派发动作，也就是把`subscribe`收集的函数，依次遍历执行\n- subscribe: ƒ subscribe(listener) 订阅收集函数存在数组中，等待触发`dispatch`依次执行。返回一个取消订阅的函数，可以取消订阅监听。\n- getState: ƒ getState() 获取存在`createStore`函数内部闭包的对象。\n- replaceReducer: ƒ replaceReducer(nextReducer) 主要用于`redux`开发者工具，对比当前和上一次操作的异同。有点类似时间穿梭功能。\n- Symbol(observable): ƒ observable()\n\n也就是[官方文档redux.org.js](https://redux.org.js)上的 `API`。\n\n暂时不去深究每一个`API`的实现。重新按`F5`刷新页面，断点到`var store = Redux.createStore(counter)`。一直按`F11`，先走一遍主流程。\n\n### 4.1 Redux.createSotre\n\n`createStore` 函数结构是这样的，是不是看起来很简单，最终返回对象`store`，包含`dispatch`、`subscribe`、`getState`、`replaceReducer`等方法。\n\n```js\n// 省略了若干代码\nexport default function createStore(reducer, preloadedState, enhancer) {\n    // 省略参数校验和替换\n    // 当前的 reducer 函数\n    let currentReducer = reducer\n    // 当前state\n    let currentState = preloadedState\n    // 当前的监听数组函数\n    let currentListeners = []\n    // 下一个监听数组函数\n    let nextListeners = currentListeners\n    // 是否正在dispatch中\n    let isDispatching = false\n    function ensureCanMutateNextListeners() {\n        if (nextListeners === currentListeners) {\n        nextListeners = currentListeners.slice()\n        }\n    }\n    function getState() {\n        return currentState\n    }\n    function subscribe(listener) {}\n    function dispatch(action) {}\n    function replaceReducer(nextReducer) {}\n    function observable() {}\n    // ActionTypes.INIT @@redux/INITu.v.d.u.6.r\n    dispatch({ type: ActionTypes.INIT })\n    return {\n        dispatch,\n        subscribe,\n        getState,\n        replaceReducer,\n        [$$observable]: observable\n    }\n}\n```\n\n### 4.2 store.dispatch(action)\n\n```js\nfunction dispatch(action) {\n    // 判断action是否是对象，不是则报错\n    if (!isPlainObject(action)) {\n      throw new Error(\n        'Actions must be plain objects. ' +\n          'Use custom middleware for async actions.'\n      )\n    }\n    // 判断action.type 是否存在，没有则报错\n    if (typeof action.type === 'undefined') {\n      throw new Error(\n        'Actions may not have an undefined \"type\" property. ' +\n          'Have you misspelled a constant?'\n      )\n    }\n    // 不是则报错\n    if (isDispatching) {\n      throw new Error('Reducers may not dispatch actions.')\n    }\n\n    try {\n      isDispatching = true\n      currentState = currentReducer(currentState, action)\n    } finally {\n        // 调用完后置为 false\n      isDispatching = false\n    }\n    //  把 收集的函数拿出来依次调用\n    const listeners = (currentListeners = nextListeners)\n    for (let i = 0; i \u003c listeners.length; i++) {\n      const listener = listeners[i]\n      listener()\n    }\n    // 最终返回 action\n    return action\n  }\n```\n\n```js\nvar store = Redux.createStore(counter)\n```\n\n上文调试完了这句。\n\n继续按`F11`调试。\n\n```js\nfunction render() {\n    valueEl.innerHTML = store.getState().toString()\n}\nrender()\n```\n\n### 4.3 store.getState()\n\n`getState`函数实现比较简单。\n\n```js\nfunction getState() {\n    // 判断正在dispatch中，则报错\n    if (isDispatching) {\n        throw new Error(\n        'You may not call store.getState() while the reducer is executing. ' +\n            'The reducer has already received the state as an argument. ' +\n            'Pass it down from the top reducer instead of reading it from the store.'\n        )\n    }\n    // 返回当前的state\n    return currentState\n}\n```\n\n### 4.4 store.subscribe(listener)\n\n订阅监听函数，存放在数组中，`store.dispatch(action)`时遍历执行。\n\n```js\nfunction subscribe(listener) {\n    // 订阅参数校验不是函数报错\n    if (typeof listener !== 'function') {\n      throw new Error('Expected the listener to be a function.')\n    }\n    // 正在dispatch中，报错\n    if (isDispatching) {\n      throw new Error(\n        'You may not call store.subscribe() while the reducer is executing. ' +\n          'If you would like to be notified after the store has been updated, subscribe from a ' +\n          'component and invoke store.getState() in the callback to access the latest state. ' +\n          'See https://redux.js.org/api-reference/store#subscribelistener for more details.'\n      )\n    }\n    // 订阅为 true\n    let isSubscribed = true\n\n    ensureCanMutateNextListeners()\n    nextListeners.push(listener)\n\n    // 返回一个取消订阅的函数\n    return function unsubscribe() {\n      if (!isSubscribed) {\n        return\n      }\n      // 正在dispatch中，则报错\n      if (isDispatching) {\n        throw new Error(\n          'You may not unsubscribe from a store listener while the reducer is executing. ' +\n            'See https://redux.js.org/api-reference/store#subscribelistener for more details.'\n        )\n      }\n      // 订阅为 false\n      isSubscribed = false\n\n      ensureCanMutateNextListeners()\n    //   找到当前监听函数\n      const index = nextListeners.indexOf(listener)\n    //   在数组中删除\n      nextListeners.splice(index, 1)\n      currentListeners = null\n    }\n  }\n```\n\n到这里，我们就调试学习完了`Redux.createSotre`、`store.dispatch`、`store.getState`、`store.subscribe`的源码。\n\n接下来，我们写个中间件例子，来调试中间件相关源码。\n\n## 5. Redux 中间件相关源码\n\n中间件是重点，面试官也经常问这类问题。\n\n### 5.1 Redux.applyMiddleware(...middlewares)\n\n#### 5.1.1 准备 logger 例子调试\n\n为了调试`Redux.applyMiddleware(...middlewares)`，我在`examples/js/middlewares.logger.example.js`写一个简单的`logger`例子。分别有三个`logger1`，`logger2`，`logger3`函数。由于都是类似，所以我在这里只展示`logger1`函数。\n\n```js\n// examples/js/middlewares.logger.example.js\nfunction logger1({ getState }) {\n  return next =\u003e action =\u003e {\n      console.log('will dispatch--1--next, action:', next, action)\n\n      // Call the next dispatch method in the middleware chain.\n      const returnValue = next(action)\n\n      console.log('state after dispatch--1', getState())\n\n      // This will likely be the action itself, unless\n      // a middleware further in chain changed it.\n      return returnValue\n  }\n}\n// 省略 logger2、logger3\n```\n\n`logger`中间件函数做的事情也比较简单，返回两层函数，`next`就是下一个中间件函数，调用返回结果。为了让读者能看懂，我把`logger1`用箭头函数、`logger2`则用普通函数。\n\n`写好例子后`，我们接着来看怎么调试`Redux.applyMiddleware(...middlewares))`源码。\n\n```bash\ncd redux-analysis \u0026\u0026 hs -p 5000\n# 上文说过npm i -g http-server\n```\n\n打开`http://localhost:5000/examples/index.2.redux.applyMiddleware.compose.html`，按`F12`打开控制台，\n\n先点击加号操作+1，把结果展示出来。\n![redux 中间件调试图](./images/redux-middlewares.png)\n从图中可以看出，`next`则是下一个函数。先1-2-3，再3-2-1这样的顺序。\n\n这种也就是我们常说的中间件，面向切面编程（AOP）。\n![中间件图解](./images/middleware.png)\n\n接下来调试，在以下语句打上断点和一些你觉得重要的地方打上断点。\n\n```js\n// examples/index.2.redux.applyMiddleware.compose.html\nvar store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))\n```\n\n#### 5.1.2 Redux.applyMiddleware(...middlewares) 源码\n\n```js\n// redux/src/applyMiddleware.js\n/**\n * ...\n * @param {...Function} middlewares The middleware chain to be applied.\n * @returns {Function} A store enhancer applying the middleware.\n */\nexport default function applyMiddleware(...middlewares) {\n  return createStore =\u003e (...args) =\u003e {\n    const store = createStore(...args)\n    let dispatch = () =\u003e {\n      throw new Error(\n        'Dispatching while constructing your middleware is not allowed. ' +\n          'Other middleware would not be applied to this dispatch.'\n      )\n    }\n\n    const middlewareAPI = {\n      getState: store.getState,\n      dispatch: (...args) =\u003e dispatch(...args)\n    }\n    const chain = middlewares.map(middleware =\u003e middleware(middlewareAPI))\n    dispatch = compose(...chain)(store.dispatch)\n\n    return {\n      ...store,\n      dispatch\n    }\n  }\n}\n```\n\n```js\n// redux/src/createStore.js\nexport default function createStore(reducer, preloadedState, enhancer) {\n  // 省略参数校验\n  // 如果第二个参数`preloadedState`是函数，并且第三个参数`enhancer`是undefined，把它们互换一下。\n  if (typeof preloadedState === 'function' \u0026\u0026 typeof enhancer === 'undefined') {\n    enhancer = preloadedState\n    preloadedState = undefined\n  }\n\n  if (typeof enhancer !== 'undefined') {\n    if (typeof enhancer !== 'function') {\n      throw new Error('Expected the enhancer to be a function.')\n    }\n    // enhancer 也就是`Redux.applyMiddleware`返回的函数\n    // createStore 的 args 则是 `reducer, preloadedState`\n    /**\n     * createStore =\u003e (...args) =\u003e {\n            const store = createStore(...args)\n            return {\n              ...store,\n               dispatch,\n            }\n        }\n     ** /\n    // 最终返回增强的store对象。\n    return enhancer(createStore)(reducer, preloadedState)\n  }\n  // 省略后续代码\n}\n```\n\n把接收的中间件函数`logger1`, `logger2`, `logger3`放入到 了`middlewares`数组中。`Redux.applyMiddleware`最后返回两层函数。\n把中间件函数都混入了参数`getState`和`dispatch`。\n\n```js\n// examples/index.2.redux.applyMiddleware.compose.html\nvar store = Redux.createStore(counter, Redux.applyMiddleware(logger1, logger2,  logger3))\n```\n\n最后这句其实是返回一个增强了`dispatch`的`store`对象。\n\n而增强的`dispatch`函数，则是用`Redux.compose(...functions)`进行串联起来执行的。\n\n### 5.2 Redux.compose(...functions)\n\n```js\nexport default function compose(...funcs) {\n  if (funcs.length === 0) {\n    return arg =\u003e arg\n  }\n\n  if (funcs.length === 1) {\n    return funcs[0]\n  }\n\n  return funcs.reduce((a, b) =\u003e (...args) =\u003e a(b(...args)))\n}\n```\n\n```js\n// applyMiddleware.js\ndispatch = compose(...chain)(store.dispatch)\n// compose\nfuncs.reduce((a, b) =\u003e (...args) =\u003e a(b(...args)))\n```\n\n这两句可能不是那么好理解，可以断点多调试几次。我把箭头函数转换成普通函数。\n\n```js\nfuncs.reduce(function(a, b){\n  return function(...args){\n    return a(b(...args));\n  };\n});\n```\n\n其实`redux`源码中注释很清晰了，这个`compose`函数上方有一堆注释，其中有一句：组合多个函数，从右到左，比如：`compose(f, g, h)` 最终得到这个结果 `(...args) =\u003e f(g(h(...args)))`.\n\n#### 5.2.1 compose 函数演化\n\n看`Redux.compose(...functions)`函数源码后，还是不明白，不要急不要慌，吃完鸡蛋还有汤。仔细来看如何演化而来，先来简单看下如下需求。\n\n传入一个数值，计算数值乘以10再加上10，再减去2。\n\n实现起来很简单。\n\n```js\nconst calc = (num) =\u003e num * 10 + 10 - 2;\ncalc(10); // 108\n```\n\n但这样写有个问题，不好扩展，比如我想乘以`10`时就打印出结果。\n为了便于扩展，我们分开写成三个函数。\n\n```js\nconst multiply = (x) =\u003e {\n   const result = x * 10;\n   console.log(result);\n   return result;\n};\nconst add = (y) =\u003e y + 10;\nconst minus = (z) =\u003e z - 2;\n\n// 计算结果\nconsole.log(minus(add(multiply(10))));\n// 100\n// 108\n// 这样我们就把三个函数计算结果出来了。\n```\n\n再来实现一个相对通用的函数，计算这三个函数的结果。\n\n```js\nconst compose = (f, g, h) =\u003e {\n  return function(x){\n    return f(g(h(x)));\n  }\n}\nconst calc = compose(minus, add, multiply);\nconsole.log(calc(10));\n// 100\n// 108\n```\n\n这样还是有问题，只支持三个函数。我想支持多个函数。\n我们了解到数组的`reduce`方法就能实现这样的功能。\n前一个函数\n\n```js\n// 我们常用reduce来计算数值数组的总和\n[1,2,3,4,5].reduce((pre, item, index, arr) =\u003e {\n  console.log('(pre, item, index, arr)', pre, item, index, arr);\n  // (pre, item, index, arr) 1 2 1 (5) [1, 2, 3, 4, 5]\n  // (pre, item, index, arr) 3 3 2 (5) [1, 2, 3, 4, 5]\n  // (pre, item, index, arr) 6 4 3 (5) [1, 2, 3, 4, 5]\n  // (pre, item, index, arr) 10 5 4 (5) [1, 2, 3, 4, 5]\n  return pre + item;\n});\n// 15\n```\n\n`pre` 是上一次返回值，在这里是数值`1,3,6,10`。在下一个例子中则是匿名函数。\n\n```js\nfunction(x){\n  return a(b(x));\n}\n```\n\n`item`是`2,3,4,5`，在下一个例子中是`minus、add、multiply`。\n\n```js\nconst compose = (...funcs) =\u003e {\n  return funcs.reduce((a, b) =\u003e {\n    return function(x){\n      return a(b(x));\n    }\n  })\n}\nconst calc = compose(minus, add, multiply);\nconsole.log(calc(10));\n// 100\n// 108\n```\n\n而`Redux.compose(...functions)`其实就是这样，只不过中间件是返回双层函数罢了。\n\n所以返回的是`next函数`，他们串起来执行了，形成了中间件的洋葱模型。\n人们都说一图胜千言。我画了一个相对简单的`redux`中间件原理图。\n\n![`redux`中间件原理图](./images/redux.middleware.drawio.png)\n\n如果还不是很明白，建议按照我给出的例子，多调试。\n\n```bash\ncd redux-analysis \u0026\u0026 hs -p 5000\n# 上文说过npm i -g http-server\n```\n\n打开`http://localhost:5000/examples/index.3.html`，按`F12`打开控制台调试。\n\n#### 5.2.2 前端框架的 compose 函数的实现\n\n**lodash**源码中 `compose`函数的实现，也是类似于数组的`reduce`，只不过是内部实现的`arrayReduce`\n\n[引用自我的文章：学习lodash源码整体架构](https://juejin.im/post/5d767e1d6fb9a06b032025ea#heading-20)\n\n```js\n// lodash源码\nfunction baseWrapperValue(value, actions) {\n\tvar result = value;\n\t// 如果是lazyWrapper的实例，则调用LazyWrapper.prototype.value 方法，也就是 lazyValue 方法\n\tif (result instanceof LazyWrapper) {\n\t\tresult = result.value();\n\t}\n\t// 类似 [].reduce()，把上一个函数返回结果作为参数传递给下一个函数\n\treturn arrayReduce(actions, function(result, action) {\n\t\treturn action.func.apply(action.thisArg, arrayPush([result], action.args));\n\t}, result);\n}\n```\n\n**koa-compose**源码也有`compose`函数的实现。实现是循环加`promise`。\n由于代码比较长我就省略了，具体看链接[若川：学习 koa 源码的整体架构，浅析koa洋葱模型原理和co原理](https://juejin.im/post/5e69925cf265da571e262fe6#heading-7)小节 `koa-compose 源码`（洋葱模型实现）\n\n## 6. Redux.combineReducers(reducers)\n\n打开`http://localhost:5000/examples/index.4.html`，按`F12`打开控制台，按照给出的例子，调试接下来的`Redux.combineReducers(reducers)`和`Redux.bindActionCreators(actionCreators, dispatch)`具体实现。由于文章已经很长了，这两个函数就不那么详细解释了。\n\n`combineReducers`函数简单来说就是合并多个`reducer`为一个函数`combination`。\n\n```js\nexport default function combineReducers(reducers) {\n  const reducerKeys = Object.keys(reducers)\n  const finalReducers = {}\n  for (let i = 0; i \u003c reducerKeys.length; i++) {\n    const key = reducerKeys[i]\n\n    // 省略一些开发环境判断的代码...\n\n    if (typeof reducers[key] === 'function') {\n      finalReducers[key] = reducers[key]\n    }\n  }\n\n  // 经过一些处理后得到最后的finalReducerKeys\n  const finalReducerKeys = Object.keys(finalReducers)\n\n  // 省略一些开发环境判断的代码...\n\n  return function combination(state = {}, action) {\n    // ... 省略开发环境的一些判断\n\n   // 用 hasChanged变量 记录前后 state 是否已经修改\n    let hasChanged = false\n    // 声明对象来存储下一次的state\n    const nextState = {}\n    //遍历 finalReducerKeys\n    for (let i = 0; i \u003c finalReducerKeys.length; i++) {\n      const key = finalReducerKeys[i]\n      const reducer = finalReducers[key]\n      const previousStateForKey = state[key]\n      // 执行 reducer\n      const nextStateForKey = reducer(previousStateForKey, action)\n\n      // 省略容错代码 ...\n\n      nextState[key] = nextStateForKey\n      // 两次 key 对比 不相等则发生改变\n      hasChanged = hasChanged || nextStateForKey !== previousStateForKey\n    }\n    // 最后的 keys 数组对比 不相等则发生改变\n    hasChanged =\n      hasChanged || finalReducerKeys.length !== Object.keys(state).length\n    return hasChanged ? nextState : state\n  }\n}\n```\n\n## 7. Redux.bindActionCreators(actionCreators, dispatch)\n\n如果第一个参数是一个函数，那就直接返回一个函数。如果是一个对象，则遍历赋值，最终生成`boundActionCreators`对象。\n\n```js\nfunction bindActionCreator(actionCreator, dispatch) {\n  return function() {\n    return dispatch(actionCreator.apply(this, arguments))\n  }\n}\n\nexport default function bindActionCreators(actionCreators, dispatch) {\n  if (typeof actionCreators === 'function') {\n    return bindActionCreator(actionCreators, dispatch)\n  }\n\n  // ... 省略一些容错判断\n\n  const boundActionCreators = {}\n  for (const key in actionCreators) {\n    const actionCreator = actionCreators[key]\n    if (typeof actionCreator === 'function') {\n      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)\n    }\n  }\n  return boundActionCreators\n}\n```\n\n`redux`所提供的的`API` 除了`store.replaceReducer(nextReducer)`没分析，其他都分析了。\n\n## 8. vuex 和 redux 简单对比\n\n### 8.1 源码实现形式\n\n从源码实现上来看，`vuex`源码主要使用了构造函数，而`redux`则是多用函数式编程、闭包。\n\n### 8.2 耦合度\n\n`vuex` 与 `vue` 强耦合，脱离了`vue`则无法使用。而`redux`跟`react`没有关系，所以它可以使用于小程序或者`jQuery`等。如果需要和`react`使用，还需要结合`react-redux`库。\n\n### 8.3 扩展\n\n```js\n// logger 插件，具体实现省略\nfunction logger (store) {\n  console.log('store', store);\n}\n// 作为数组传入\nnew Vuex.Store({\n  state,\n  getters,\n  actions,\n  mutations,\n  plugins: process.env.NODE_ENV !== 'production'\n    ? [logger]\n    : []\n})\n// vuex 源码 插件执行部分\nclass Store{\n  constructor(){\n    // 把vuex的实例对象 store整个对象传递给插件使用\n    plugins.forEach(plugin =\u003e plugin(this))\n  }\n}\n```\n\n`vuex`实现扩展则是使用插件形式，而`redux`是中间件的形式。`redux`的中间件则是AOP（面向切面编程），`redux`中`Redux.applyMiddleware()`其实也是一个增强函数，所以也可以用户来实现增强器，所以[`redux`生态](https://www.redux.org.cn/docs/introduction/Ecosystem.html)比较繁荣。\n\n### 8.4 上手难易度\n\n相对来说，`vuex`上手相对简单，`redux`相对难一些，`redux`涉及到一些函数式编程、高阶函数、纯函数等概念。\n\n## 9. 总结\n\n文章主要通过一步步调试的方式循序渐进地讲述`redux`源码的具体实现。旨在教会读者调试源码，不惧怕源码。\n\n面试官经常喜欢考写一个`redux`中间件，说说`redux`中间件的原理。\n\n```js\nfunction logger1({ getState }) {\n  return next =\u003e action =\u003e {\n      const returnValue = next(action)\n      return returnValue\n  }\n}\n```\n\n```js\nconst compose = (...funcs) =\u003e {\n  if (funcs.length === 0) {\n    return arg =\u003e arg\n  }\n\n  if (funcs.length === 1) {\n    return funcs[0]\n  }\n\n  // 箭头函数\n  // return funcs.reduce((a, b) =\u003e (...args) =\u003e a(b(...args)))\n  return funcs.reduce((a, b) =\u003e {\n    return function(x){\n      return a(b(x));\n    }\n  })\n}\n```\n\n```js\nconst enhancerStore = Redux.create(reducer, Redux.applyMiddleware(logger1, ...))\nenhancerStore.dispatch(action)\n```\n\n用户触发`enhancerStore.dispatch(action)`是增强后的，其实就是第一个中间件函数，中间的`next`是下一个中间件函数，最后`next`是没有增强的`store.dispatch(action)`。\n\n最后再来看张`redux`工作流程图 ![`redux`工作流程图](./images/redux-workflow-gif.png)是不是就更理解些了呢。\n\n如果读者发现有不妥或可改善之处，再或者哪里没写明白的地方，欢迎评论指出。另外觉得写得不错，对你有些许帮助，可以点赞、评论、转发分享，也是对我的一种支持，非常感谢呀。**要是有人说到怎么读源码，正在读文章的你能推荐我的源码系列文章，那真是太好了**。\n\n## 推荐阅读\n\n[@胡子大哈：动手实现 Redux（一）：优雅地修改共享状态](http://huziketang.mangojuice.top/books/react/lesson30)，总共6小节，非常推荐，虽然我很早前就看完了《react小书》，现在再看一遍又有收获\u003cbr\u003e\n[美团@莹莹 Redux从设计到源码](https://tech.meituan.com/2017/07/14/redux-design-code.html)，美团这篇是我基本写完文章后看到的，感觉写得很好，非常推荐\u003cbr\u003e\n[redux 中文文档](https://www.redux.org.cn/)\u003cbr\u003e\n[redux 英文文档](https://redux.js.org)\u003cbr\u003e\n[若川的学习redux源码仓库](http://github.com/lxchuan12/redux-analysis.git)\n\n## 另一个系列\n\n[面试官问：JS的继承](https://juejin.im/post/5c433e216fb9a049c15f841b)\u003cbr\u003e\n[面试官问：JS的this指向](https://juejin.im/post/5c0c87b35188252e8966c78a)\u003cbr\u003e\n[面试官问：能否模拟实现JS的call和apply方法](https://juejin.im/post/5bf6c79bf265da6142738b29)\u003cbr\u003e\n[面试官问：能否模拟实现JS的bind方法](https://juejin.im/post/5bec4183f265da616b1044d7)\u003cbr\u003e\n[面试官问：能否模拟实现JS的new操作符](https://juejin.im/post/5bde7c926fb9a049f66b8b52)\u003cbr\u003e\n\n## 关于\n\n作者：常以**若川**为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少，唯善学。\u003cbr\u003e\n[若川的博客](https://lxchuan12.cn)，使用`vuepress`重构了，阅读体验可能更好些\u003cbr\u003e\n[掘金专栏](https://juejin.im/user/57974dc55bbb500063f522fd/posts)，欢迎关注~\u003cbr\u003e\n[`segmentfault`前端视野专栏](https://segmentfault.com/blog/lxchuan12)，欢迎关注~\u003cbr\u003e\n[知乎前端视野专栏](https://zhuanlan.zhihu.com/lxchuan12)，欢迎关注~\u003cbr\u003e\n[语雀前端视野专栏](https://www.yuque.com/lxchuan12/blog)，新增语雀专栏，欢迎关注~\u003cbr\u003e\n[github blog](https://github.com/lxchuan12/blog)，相关源码和资源都放在这里，求个`star`^_^~\n\n## 欢迎加微信交流 微信公众号\n\n可能比较有趣的微信公众号，长按扫码关注（**回复pdf获取前端优质书籍pdf**）。欢迎加我微信`ruochuan12`（注明来源，基本来者不拒），拉你进【前端视野交流群】，长期交流学习~\n\n![若川视野](https://github.com/lxchuan12/blog/raw/master/docs/about/wechat-official-accounts-mini.jpg)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fredux-analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fruochuan12%2Fredux-analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fruochuan12%2Fredux-analysis/lists"}