payload: {"action":"created","issue":{"url":"https://api.github.com/repos/brickspert/blog/issues/22","repository_url":"https://api.github.com/repos/brickspert/blog","labels_url":"https://api.github.com/repos/brickspert/blog/issues/22/labels{/name}","comments_url":"https://api.github.com/repos/brickspert/blog/issues/22/comments","events_url":"https://api.github.com/repos/brickspert/blog/issues/22/events","html_url":"https://github.com/brickspert/blog/issues/22","id":379514589,"node_id":"MDU6SXNzdWUzNzk1MTQ1ODk=","number":22,"title":"完全理解 redux（从零实现一个 redux）","user":{"login":"brickspert","id":12526493,"node_id":"MDQ6VXNlcjEyNTI2NDkz","avatar_url":"https://avatars0.githubusercontent.com/u/12526493?v=4","gravatar_id":"","url":"https://api.github.com/users/brickspert","html_url":"https://github.com/brickspert","followers_url":"https://api.github.com/users/brickspert/followers","following_url":"https://api.github.com/users/brickspert/following{/other_user}","gists_url":"https://api.github.com/users/brickspert/gists{/gist_id}","starred_url":"https://api.github.com/users/brickspert/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/brickspert/subscriptions","organizations_url":"https://api.github.com/users/brickspert/orgs","repos_url":"https://api.github.com/users/brickspert/repos","events_url":"https://api.github.com/users/brickspert/events{/privacy}","received_events_url":"https://api.github.com/users/brickspert/received_events","type":"User","site_admin":false},"labels":[],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":24,"created_at":"2018-11-11T11:41:48Z","updated_at":"2019-07-19T08:28:58Z","closed_at":null,"author_association":"OWNER","body":"# 完全理解 redux\r\n\r\n> 交流加QQ群【572071150】 备注【前端】\r\n\r\n### 目录\r\n\r\n1. [前言](#intro)\r\n2. [状态管理器](#state)\r\n   - 简单的状态管理器\r\n   - 有计划的状态管理器\r\n3. [多文件协作](#coordination)\r\n   - reducer 的拆分和合并\r\n   - state 的拆分和合并\r\n4. [中间件 middleware](#middleware)\r\n5. [完整的 redux](#redux)\r\n6. [最佳实践](#practice)\r\n   - 纯函数\r\n7. [总结](#summary)\r\n\r\n###  <div id=\"intro\">前言</div>\r\n\r\n记得开始接触 react 技术栈的时候，最难理解的地方就是 redux。全是新名词：reducer、store、dispatch、middleware 等等，我就理解 state 一个名词。\r\n\r\n网上找的 redux 文章，要不有一本书的厚度，要不很玄乎，晦涩难懂，越看越觉得难，越看越怕，信心都没有了！\r\n\r\n花了很长时间熟悉 redux，慢慢的发现它其实真的很简单。本章不会把 redux 的各种概念，名词解释一遍，这样和其他教程没有任何区别，没有太大意义。我会带大家从零实现一个完整的 redux，让大家知其然，知其所以然。\r\n\r\n开始前，你必须知道一些事情：\r\n\r\n- redux 和 react 没有关系，redux 可以用在任何框架中，忘掉 react。\r\n- connect 不属于 redux，它其实属于 react-redux，请先忘掉它，下一章节，我们会介绍它。\r\n- 请一定先忘记 reducer、store、dispatch、middleware 等等这些名词。\r\n- redux 是一个状态管理器。\r\n\r\nLet's Go！\r\n\r\n### <div id=\"state\">状态管理器</div>\r\n\r\n#### 简单的状态管理器\r\n\r\nredux 是一个状态管理器，那什么是状态呢？状态就是数据，比如计数器中的 count。\r\n\r\n```javascript\r\nlet state = {\r\n  count: 1\r\n}\r\n```\r\n\r\n我们来使用下状态\r\n\r\n```javascript\r\nconsole.log(state.count);\r\n```\r\n\r\n我们来修改下状态\r\n\r\n```javascript\r\nstate.count = 2;\r\n```\r\n\r\n好了，现在我们实现了状态（计数）的修改和使用了。\r\n\r\n> 读者：你当我傻吗？你说的这个谁不知道？捶你👊！\r\n>\r\n>  笔者：哎哎哎，别打我！有话好好说！redux 核心就是这个呀！我们一步一步扩展开来嘛！\r\n\r\n当然上面的有一个很明显的问题：修改 count 之后，使用 count 的地方不能收到通知。我们可以使用发布-订阅模式来解决这个问题。\r\n\r\n```javascript\r\n/*------count 的发布订阅者实践------*/\r\nlet state = {\r\n  count: 1\r\n};\r\nlet listeners = [];\r\n\r\n/*订阅*/\r\nfunction subscribe(listener) {\r\n  listeners.push(listener);\r\n}\r\n\r\nfunction changeCount(count) {\r\n  state.count = count;\r\n  /*当 count 改变的时候，我们要去通知所有的订阅者*/\r\n  for (let i = 0; i < listeners.length; i++) {\r\n    const listener = listeners[i];\r\n    listener();\r\n  }\r\n}\r\n```\r\n\r\n我们来尝试使用下这个简单的计数状态管理器。\r\n\r\n```javascript\r\n/*来订阅一下，当 count 改变的时候，我要实时输出新的值*/\r\nsubscribe(() => {\r\n  console.log(state.count);\r\n});\r\n\r\n/*我们来修改下 state，当然我们不能直接去改 state 了，我们要通过 changeCount 来修改*/\r\nchangeCount(2);\r\nchangeCount(3);\r\nchangeCount(4);\r\n```\r\n\r\n现在我们可以看到，我们修改 count 的时候，会输出相应的 count 值。\r\n\r\n现在有两个新的问题摆在我们面前\r\n\r\n- 这个状态管理器只能管理 count，不通用\r\n- 公共的代码要封装起来\r\n\r\n我们尝试来解决这个问题，把公共的代码封装起来\r\n\r\n```javascript\r\nconst createStore = function (initState) {\r\n  let state = initState;\r\n  let listeners = [];\r\n\r\n  /*订阅*/\r\n  function subscribe(listener) {\r\n    listeners.push(listener);\r\n  }\r\n\r\n  function changeState(newState) {\r\n    state = newState;\r\n    /*通知*/\r\n    for (let i = 0; i < listeners.length; i++) {\r\n      const listener = listeners[i];\r\n      listener();\r\n    }\r\n  }\r\n\r\n  function getState() {\r\n    return state;\r\n  }\r\n\r\n  return {\r\n    subscribe,\r\n    changeState,\r\n    getState\r\n  }\r\n}\r\n```\r\n\r\n我们来使用这个状态管理器管理多个状态 counter 和 info 试试\r\n\r\n```javascript\r\nlet initState = {\r\n  counter: {\r\n    count: 0\r\n  },\r\n  info: {\r\n    name: '',\r\n    description: ''\r\n  }\r\n}\r\n\r\nlet store = createStore(initState);\r\n\r\nstore.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(`${state.info.name}：${state.info.description}`);\r\n});\r\nstore.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(state.counter.count);\r\n});\r\n\r\nstore.changeState({\r\n  ...store.getState(),\r\n  info: {\r\n    name: '前端九部',\r\n    description: '我们都是前端爱好者！'\r\n  }\r\n});\r\n\r\nstore.changeState({\r\n  ...store.getState(),\r\n  counter: {\r\n    count: 1\r\n  }\r\n});\r\n```\r\n\r\n到这里我们完成了一个简单的状态管理器。\r\n\r\n这里需要理解的是 `createStore`，提供了 `changeState`，`getState`，`subscribe` 三个能力。\r\n\r\n本小节完整源码见 [demo-1](https://github.com/frontend9/redux-demo/tree/master/demo-1)\r\n\r\n#### 有计划的状态管理器\r\n\r\n我们用上面的状态管理器来实现一个自增，自减的计数器。\r\n\r\n```javascript\r\nlet initState = {\r\n  count: 0\r\n}\r\nlet store = createStore(initState);\r\n\r\nstore.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(state.count);\r\n});\r\n/*自增*/\r\nstore.changeState({\r\n  count: store.getState().count + 1\r\n});\r\n/*自减*/\r\nstore.changeState({\r\n  count: store.getState().count - 1\r\n});\r\n/*我想随便改*/\r\nstore.changeState({\r\n  count: 'abc'\r\n});\r\n```\r\n\r\n你一定发现了问题，count 被改成了字符串 `abc`，因为我们对 count 的修改没有任何约束，任何地方，任何人都可以修改。\r\n\r\n我们需要约束，不允许计划外的 count 修改，我们只允许 count 自增和自减两种改变方式！\r\n\r\n那我们分两步来解决这个问题\r\n\r\n1. 制定一个 state 修改计划，告诉 store，我的修改计划是什么。\r\n2. 修改 store.changeState 方法，告诉它修改 state 的时候，按照我们的计划修改。\r\n\r\n我们来设置一个 plan 函数，接收现在的 state，和一个 action，返回经过改变后的新的 state。\r\n\r\n```javascript\r\n/*注意：action = {type:'',other:''}, action 必须有一个 type 属性*/\r\nfunction plan(state, action) {\r\n  switch (action.type) {\r\n    case 'INCREMENT':\r\n      return {\r\n        ...state,\r\n        count: state.count + 1\r\n      }\r\n    case 'DECREMENT':\r\n      return {\r\n        ...state,\r\n        count: state.count - 1\r\n      }\r\n    default:\r\n      return state;\r\n  }\r\n}\r\n```\r\n\r\n我们把这个计划告诉 store，store.changeState 以后改变 state 要按照我的计划来改。\r\n\r\n```javascript\r\n/*增加一个参数 plan*/\r\nconst createStore = function (plan, initState) {\r\n  let state = initState;\r\n  let listeners = [];\r\n\r\n  function subscribe(listener) {\r\n    listeners.push(listener);\r\n  }\r\n\r\n  function changeState(action) {\r\n    /*请按照我的计划修改 state*/  \r\n    state = plan(state, action);\r\n    for (let i = 0; i < listeners.length; i++) {\r\n      const listener = listeners[i];\r\n      listener();\r\n    }\r\n  }\r\n\r\n  function getState() {\r\n    return state;\r\n  }\r\n\r\n  return {\r\n    subscribe,\r\n    changeState,\r\n    getState\r\n  }\r\n}\r\n```\r\n\r\n我们来尝试使用下新的 createStore 来实现自增和自减\r\n\r\n```javascript\r\nlet initState = {\r\n  count: 0\r\n}\r\n/*把plan函数*/\r\nlet store = createStore(plan, initState);\r\n\r\nstore.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(state.count);\r\n});\r\n/*自增*/\r\nstore.changeState({\r\n  type: 'INCREMENT'\r\n});\r\n/*自减*/\r\nstore.changeState({\r\n  type: 'DECREMENT'\r\n});\r\n/*我想随便改 计划外的修改是无效的！*/\r\nstore.changeState({\r\n  count: 'abc'\r\n});\r\n```\r\n\r\n到这里为止，我们已经实现了一个有计划的状态管理器！\r\n\r\n我们商量一下吧？我们给 plan 和 changeState 改下名字好不好？**plan 改成 reducer，changeState 改成 dispatch！**不管你同不同意，我都要换，因为新名字比较厉害（其实因为 redux 是这么叫的）!\r\n\r\n本小节完整源码见 [demo-2](https://github.com/frontend9/redux-demo/tree/master/demo-2)\r\n\r\n### <div id=\"coordination\">多文件协作</div>\r\n\r\n#### reducer 的拆分和合并\r\n\r\n这一小节我们来处理下 reducer 的问题。啥问题？\r\n\r\n我们知道 reducer 是一个计划函数，接收老的 state，按计划返回新的 state。那我们项目中，有大量的 state，每个 state 都需要计划函数，如果全部写在一起会是啥样子呢？\r\n\r\n所有的计划写在一个 reducer 函数里面，会导致 reducer 函数及其庞大复杂。按经验来说，我们肯定会按组件维度来拆分出很多个 reducer 函数，然后通过一个函数来把他们合并起来。\r\n\r\n我们来管理两个 state，一个 counter，一个 info。\r\n\r\n```javascript\r\nlet state = {\r\n  counter: {\r\n    count: 0\r\n  },\r\n  info: {\r\n    name: '前端九部',\r\n    description: '我们都是前端爱好者！'\r\n  }\r\n}\r\n```\r\n\r\n他们各自的 reducer\r\n\r\n```javascript\r\n/*counterReducer, 一个子reducer*/\r\n/*注意：counterReducer 接收的 state 是 state.counter*/\r\nfunction counterReducer(state, action) {\r\n  switch (action.type) {\r\n    case 'INCREMENT':\r\n      return {\r\n        count: state.count + 1\r\n      }\r\n    case 'DECREMENT':\r\n      return {\r\n        ...state,\r\n        count: state.count - 1\r\n      }\r\n    default:\r\n      return state;\r\n  }\r\n}\r\n```\r\n\r\n```javascript\r\n/*InfoReducer，一个子reducer*/\r\n/*注意：InfoReducer 接收的 state 是 state.info*/\r\nfunction InfoReducer(state, action) {\r\n  switch (action.type) {\r\n    case 'SET_NAME':\r\n      return {\r\n        ...state,\r\n        name: action.name\r\n      }\r\n    case 'SET_DESCRIPTION':\r\n      return {\r\n        ...state,\r\n        description: action.description\r\n      }\r\n    default:\r\n      return state;\r\n  }\r\n}\r\n```\r\n\r\n那我们用 combineReducers 函数来把多个 reducer 函数合并成一个 reducer 函数。大概这样用\r\n\r\n```javascript\r\nconst reducer = combineReducers({\r\n    counter: counterReducer,\r\n    info: InfoReducer\r\n});\r\n```\r\n\r\n我们尝试实现下 combineReducers 函数\r\n\r\n```javascript\r\nfunction combineReducers(reducers) {\r\n\r\n  /* reducerKeys = ['counter', 'info']*/\r\n  const reducerKeys = Object.keys(reducers)\r\n\r\n  /*返回合并后的新的reducer函数*/\r\n  return function combination(state = {}, action) {\r\n    /*生成的新的state*/\r\n    const nextState = {}\r\n\r\n    /*遍历执行所有的reducers，整合成为一个新的state*/\r\n    for (let i = 0; i < reducerKeys.length; i++) {\r\n      const key = reducerKeys[i]\r\n      const reducer = reducers[key]\r\n      /*之前的 key 的 state*/\r\n      const previousStateForKey = state[key]\r\n      /*执行 分 reducer，获得新的state*/\r\n      const nextStateForKey = reducer(previousStateForKey, action)\r\n\r\n      nextState[key] = nextStateForKey\r\n    }\r\n    return nextState;\r\n  }\r\n}\r\n```\r\n\r\n我们来尝试下 combineReducers 的威力吧\r\n\r\n```javascript\r\nconst reducer = combineReducers({\r\n  counter: counterReducer,\r\n  info: InfoReducer\r\n});\r\n\r\nlet initState = {\r\n  counter: {\r\n    count: 0\r\n  },\r\n  info: {\r\n    name: '前端九部',\r\n    description: '我们都是前端爱好者！'\r\n  }\r\n}\r\n\r\nlet store = createStore(reducer, initState);\r\n\r\nstore.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(state.counter.count, state.info.name, state.info.description);\r\n});\r\n/*自增*/\r\nstore.dispatch({\r\n  type: 'INCREMENT'\r\n});\r\n\r\n/*修改 name*/\r\nstore.dispatch({\r\n  type: 'SET_NAME',\r\n  name: '前端九部2号'\r\n});\r\n```\r\n\r\n本小节完整源码见 [demo-3](https://github.com/frontend9/redux-demo/tree/master/demo-3)\r\n\r\n#### state 的拆分和合并\r\n\r\n上一小节，我们把 reducer 按组件维度拆分了，通过 combineReducers 合并了起来。但是还有个问题， state 我们还是写在一起的，这样会造成 state 树很庞大，不直观，很难维护。我们需要拆分，一个 state，一个 reducer 写一块。\r\n\r\n这一小节比较简单，我就不卖关子了，用法大概是这样（注意注释）\r\n\r\n```javascript\r\n/* counter 自己的 state 和 reducer 写在一起*/\r\nlet initState = {\r\n  count: 0\r\n}\r\nfunction counterReducer(state, action) {\r\n  /*注意：如果 state 没有初始值，那就给他初始值！！*/  \r\n  if (!state) {\r\n      state = initState;\r\n  }\r\n  switch (action.type) {\r\n    case 'INCREMENT':\r\n      return {\r\n        count: state.count + 1\r\n      }\r\n    default:    \r\n      return state;\r\n  }\r\n}\r\n```\r\n\r\n我们修改下 createStore 函数，增加一行 `dispatch({ type: Symbol() })`\r\n\r\n```javascript\r\nconst createStore = function (reducer, initState) {\r\n  let state = initState;\r\n  let listeners = [];\r\n\r\n  function subscribe(listener) {\r\n    listeners.push(listener);\r\n  }\r\n\r\n  function dispatch(action) {\r\n    state = reducer(state, action);\r\n    for (let i = 0; i < listeners.length; i++) {\r\n      const listener = listeners[i];\r\n      listener();\r\n    }\r\n  }\r\n\r\n  function getState() {\r\n    return state;\r\n  }\r\n  /* 注意！！！只修改了这里，用一个不匹配任何计划的 type，来获取初始值 */\r\n  dispatch({ type: Symbol() })\r\n\r\n  return {\r\n    subscribe,\r\n    dispatch,\r\n    getState\r\n  }\r\n}\r\n```\r\n\r\n我们思考下这行可以带来什么效果？\r\n\r\n1. createStore 的时候，用一个不匹配任何 type 的 action，来触发 `state = reducer(state, action)`\r\n2. 因为 action.type 不匹配，每个子 reducer 都会进到 default 项，返回自己初始化的 state，这样就获得了初始化的 state 树了。\r\n\r\n你可以试试\r\n\r\n```javascript\r\n/*这里没有传 initState 哦 */\r\nconst store = createStore(reducer);\r\n/*这里看看初始化的 state 是什么*/\r\nconsole.dir(store.getState());\r\n```\r\n\r\n本小节完整源码见 [demo-4](https://github.com/frontend9/redux-demo/tree/master/demo-4)\r\n\r\n到这里为止，我们已经实现了一个七七八八的 redux 啦！\r\n\r\n### <div id=\"middleware\">中间件 middleware</div>\r\n\r\n中间件 middleware 是 redux 中最难理解的地方。但是我挑战一下用最通俗的语言来讲明白它。如果你看完这一小节，还没明白中间件是什么，不知道如何写一个中间件，那就是我的锅了！\r\n\r\n中间件是对 dispatch 的扩展，或者说重写，增强 dispatch 的功能！\r\n\r\n#### 记录日志\r\n\r\n我现在有一个需求，在每次修改 state 的时候，记录下来 修改前的 state ，为什么修改了，以及修改后的 state。我们可以通过重写 store.dispatch 来实现，直接看代码\r\n\r\n```javascript\r\nconst store = createStore(reducer);\r\nconst next = store.dispatch;\r\n\r\n/*重写了store.dispatch*/\r\nstore.dispatch = (action) => {\r\n  console.log('this state', store.getState());\r\n  console.log('action', action);\r\n  next(action);\r\n  console.log('next state', store.getState());\r\n}\r\n```\r\n\r\n我们来使用下\r\n\r\n```javascript\r\nstore.dispatch({\r\n  type: 'INCREMENT'\r\n});\r\n```\r\n\r\n日志输出为\r\n\r\n```javascript\r\nthis state { counter: { count: 0 } }\r\naction { type: 'INCREMENT' }\r\n1\r\nnext state { counter: { count: 1 } }\r\n```\r\n\r\n现在我们已经实现了一个完美的记录 state 修改日志的功能！\r\n\r\n#### 记录异常\r\n\r\n我又有一个需求，需要记录每次数据出错的原因，我们扩展下 dispatch\r\n\r\n```javascript\r\nconst store = createStore(reducer);\r\nconst next = store.dispatch;\r\n\r\nstore.dispatch = (action) => {\r\n  try {\r\n    next(action);\r\n  } catch (err) {\r\n    console.error('错误报告: ', err)\r\n  }\r\n}\r\n```\r\n\r\n这样每次 dispatch 出异常的时候，我们都会记录下来。\r\n\r\n#### 多中间件的合作\r\n\r\n我现在既需要记录日志，又需要记录异常，怎么办？当然很简单了，两个函数合起来呗！\r\n\r\n```javascript\r\nstore.dispatch = (action) => {\r\n  try {\r\n    console.log('this state', store.getState());\r\n    console.log('action', action);\r\n    next(action);\r\n    console.log('next state', store.getState());\r\n  } catch (err) {\r\n    console.error('错误报告: ', err)\r\n  }\r\n}\r\n```\r\n\r\n如果又来一个需求怎么办？接着改 dispatch 函数？那再来10个需求呢？到时候 dispatch 函数肯定庞大混乱到无法维护了！这个方式不可取呀！\r\n\r\n我们需要考虑如何实现扩展性很强的多中间件合作模式。\r\n\r\n1. 我们把 loggerMiddleware 提取出来\r\n\r\n   ```javascript\r\n   const store = createStore(reducer);\r\n   const next = store.dispatch;\r\n   \r\n   const loggerMiddleware = (action) => {\r\n     console.log('this state', store.getState());\r\n     console.log('action', action);\r\n     next(action);\r\n     console.log('next state', store.getState());\r\n   }\r\n   \r\n   store.dispatch = (action) => {\r\n     try {\r\n       loggerMiddleware(action);\r\n     } catch (err) {\r\n       console.error('错误报告: ', err)\r\n     }\r\n   }\r\n   ```\r\n\r\n2. 我们把 exceptionMiddleware 提取出来\r\n\r\n   ```javascript\r\n   const exceptionMiddleware = (action) => {\r\n     try {\r\n       /*next(action)*/\r\n       loggerMiddleware(action);\r\n     } catch (err) {\r\n       console.error('错误报告: ', err)\r\n     } \r\n   }\r\n   store.dispatch = exceptionMiddleware;\r\n   ```\r\n\r\n3. 现在的代码有一个很严重的问题，就是 exceptionMiddleware 里面写死了 loggerMiddleware，我们需要让 `next(action)`变成动态的，随便哪个中间件都可以\r\n\r\n   ```javascript\r\n   const exceptionMiddleware = (next) => (action) => {\r\n     try {\r\n       /*loggerMiddleware(action);*/\r\n       next(action);\r\n     } catch (err) {\r\n       console.error('错误报告: ', err)\r\n     } \r\n   }\r\n   /*loggerMiddleware 变成参数传进去*/\r\n   store.dispatch = exceptionMiddleware(loggerMiddleware);\r\n   ```\r\n\r\n4. 同样的道理，loggerMiddleware 里面的 next 现在恒等于 store.dispatch，导致 loggerMiddleware 里面无法扩展别的中间件了！我们也把 next 写成动态的\r\n\r\n   ```javascript\r\n   const loggerMiddleware = (next) => (action) => {\r\n     console.log('this state', store.getState());\r\n     console.log('action', action);\r\n     next(action);\r\n     console.log('next state', store.getState());\r\n   }\r\n   ```\r\n\r\n到这里为止，我们已经探索出了一个扩展性很高的中间件合作模式！\r\n\r\n```javascript\r\nconst store = createStore(reducer);\r\nconst next = store.dispatch;\r\n\r\nconst loggerMiddleware = (next) => (action) => {\r\n  console.log('this state', store.getState());\r\n  console.log('action', action);\r\n  next(action);\r\n  console.log('next state', store.getState());\r\n}\r\n\r\nconst exceptionMiddleware = (next) => (action) => {\r\n  try {\r\n    next(action);\r\n  } catch (err) {\r\n    console.error('错误报告: ', err)\r\n  }\r\n}\r\n\r\nstore.dispatch = exceptionMiddleware(loggerMiddleware(next));\r\n```\r\n\r\n这时候我们开开心心的新建了一个 `loggerMiddleware.js`，一个`exceptionMiddleware.js`文件，想把两个中间件独立到单独的文件中去。会碰到什么问题吗？\r\n\r\nloggerMiddleware 中包含了外部变量 store，导致我们无法把中间件独立出去。那我们把 store 也作为一个参数传进去好了~\r\n\r\n```javascript\r\nconst store = createStore(reducer);\r\nconst next  = store.dispatch;\r\n\r\nconst loggerMiddleware = (store) => (next) => (action) => {\r\n  console.log('this state', store.getState());\r\n  console.log('action', action);\r\n  next(action);\r\n  console.log('next state', store.getState());\r\n}\r\n\r\nconst exceptionMiddleware = (store) => (next) => (action) => {\r\n  try {\r\n    next(action);\r\n  } catch (err) {\r\n    console.error('错误报告: ', err)\r\n  }\r\n}\r\n\r\nconst logger = loggerMiddleware(store);\r\nconst exception = exceptionMiddleware(store);\r\nstore.dispatch = exception(logger(next));\r\n```\r\n\r\n到这里为止，我们真正的实现了两个可以独立的中间件啦！\r\n\r\n现在我有一个需求，在打印日志之前输出当前的时间戳。用中间件来实现！\r\n\r\n```javascript\r\nconst timeMiddleware = (store) => (next) => (action) => {\r\n  console.log('time', new Date().getTime());\r\n  next(action);\r\n}\r\n...\r\nconst time = timeMiddleware(store);\r\nstore.dispatch = exception(time(logger(next)));\r\n```\r\n\r\n本小节完整源码见 [demo-6](https://github.com/frontend9/redux-demo/tree/master/demo-6)\r\n\r\n#### 中间件使用方式优化\r\n\r\n上一节我们已经完全实现了正确的中间件！但是中间件的使用方式不是很友好\r\n\r\n```javascript\r\nimport loggerMiddleware from './middlewares/loggerMiddleware';\r\nimport exceptionMiddleware from './middlewares/exceptionMiddleware';\r\nimport timeMiddleware from './middlewares/timeMiddleware';\r\n\r\n...\r\n\r\nconst store = createStore(reducer);\r\nconst next = store.dispatch;\r\n\r\nconst logger = loggerMiddleware(store);\r\nconst exception = exceptionMiddleware(store);\r\nconst time = timeMiddleware(store);\r\nstore.dispatch = exception(time(logger(next)));\r\n```\r\n\r\n其实我们只需要知道三个中间件，剩下的细节都可以封装起来！我们通过扩展 createStore 来实现！\r\n\r\n先来看看期望的用法\r\n\r\n```javascript\r\n/*接收旧的 createStore，返回新的 createStore*/\r\nconst newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);\r\n\r\n/*返回了一个 dispatch 被重写过的 store*/\r\nconst store = newCreateStore(reducer);\r\n```\r\n\r\n实现 applyMiddleware\r\n\r\n```javascript\r\nconst applyMiddleware = function (...middlewares) {\r\n  /*返回一个重写createStore的方法*/\r\n  return function rewriteCreateStoreFunc(oldCreateStore) {\r\n     /*返回重写后新的 createStore*/\r\n    return function newCreateStore(reducer, initState) {\r\n      /*1. 生成store*/\r\n      const store = oldCreateStore(reducer, initState);\r\n      /*给每个 middleware 传下store，相当于 const logger = loggerMiddleware(store);*/\r\n      /* const chain = [exception, time, logger]*/\r\n      const chain = middlewares.map(middleware => middleware(store));\r\n      let dispatch = store.dispatch;\r\n      /* 实现 exception(time((logger(dispatch))))*/\r\n      chain.reverse().map(middleware => {\r\n        dispatch = middleware(dispatch);\r\n      });\r\n\r\n      /*2. 重写 dispatch*/\r\n      store.dispatch = dispatch;\r\n      return store;\r\n    }\r\n  }\r\n}\r\n```\r\n\r\n#### 让用户体验美好\r\n\r\n现在还有个小问题，我们有两种 createStore 了\r\n\r\n```javascript\r\n/*没有中间件的 createStore*/\r\nimport { createStore } from './redux';\r\nconst store = createStore(reducer, initState);\r\n\r\n/*有中间件的 createStore*/\r\nconst rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);\r\nconst newCreateStore = rewriteCreateStoreFunc(createStore);\r\nconst store = newCreateStore(reducer, initState);\r\n```\r\n\r\n为了让用户用起来统一一些，我们可以很简单的使他们的使用方式一致，我们修改下 createStore 方法\r\n\r\n```javascript\r\nconst createStore = (reducer, initState, rewriteCreateStoreFunc) => {\r\n    /*如果有 rewriteCreateStoreFunc，那就采用新的 createStore */\r\n    if(rewriteCreateStoreFunc){\r\n       const newCreateStore =  rewriteCreateStoreFunc(createStore);\r\n       return newCreateStore(reducer, initState);\r\n    }\r\n    /*否则按照正常的流程走*/\r\n    ...\r\n}\r\n```\r\n\r\n最终的用法\r\n\r\n```javascript\r\nconst rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware);\r\n\r\nconst store = createStore(reducer, initState, rewriteCreateStoreFunc);\r\n```\r\n\r\n本小节完整源码见 [demo-7](https://github.com/frontend9/redux-demo/tree/master/demo-7)\r\n\r\n### <div id=\"redux\">完整的 redux</div>\r\n\r\n#### 退订\r\n\r\n不能退订的订阅都是耍流浪！我们修改下 store.subscribe 方法，增加退订功能\r\n\r\n```javascript\r\n  function subscribe(listener) {\r\n    listeners.push(listener);\r\n    return function unsubscribe() {\r\n      const index = listeners.indexOf(listener)\r\n      listeners.splice(index, 1)\r\n    }\r\n  }\r\n```\r\n\r\n使用\r\n\r\n```javascript\r\nconst unsubscribe = store.subscribe(() => {\r\n  let state = store.getState();\r\n  console.log(state.counter.count);\r\n});\r\n/*退订*/\r\nunsubscribe();\r\n```\r\n\r\n#### 中间件拿到的store\r\n\r\n现在的中间件拿到了完整的 store，他甚至可以修改我们的 subscribe 方法，按照最小开放策略，我们只用把 getState 给中间件就可以了！因为我们只允许你用 getState 方法！\r\n\r\n修改下 applyMiddleware 中给中间件传的 store\r\n\r\n```javascript\r\n/*const chain = middlewares.map(middleware => middleware(store));*/\r\nconst simpleStore = { getState: store.getState };\r\nconst chain = middlewares.map(middleware => middleware(simpleStore));\r\n```\r\n\r\n#### compose\r\n\r\n我们的 applyMiddleware 中，把  [A, B, C] 转换成 A(B(C(next)))，是这样实现的\r\n\r\n```javascript\r\nconst chain = [A, B, C];\r\nlet dispatch = store.dispatch;\r\nchain.reverse().map(middleware => {\r\n   dispatch = middleware(dispatch);\r\n});\r\n```\r\n\r\nredux 提供了一个 compose 方式，可以帮我们做这个事情\r\n\r\n```javascript\r\nconst chain = [A, B, C];\r\ndispatch = compose(...chain)(store.dispatch)\r\n```\r\n\r\n看下他是如何实现的\r\n\r\n```javascript\r\nexport default function compose(...funcs) {\r\n  if (funcs.length === 1) {\r\n    return funcs[0]\r\n  }\r\n  return funcs.reduce((a, b) => (...args) => a(b(...args)))\r\n}\r\n```\r\n\r\n当然 compose 函数对于新人来说可能比较难理解，你只需要他是做什么的就行啦！\r\n\r\n#### 省略initState\r\n\r\n有时候我们创建 store 的时候不传 initState，我们怎么用？\r\n\r\n```javascript\r\nconst store = createStore(reducer, {}, rewriteCreateStoreFunc);\r\n```\r\n\r\nredux 允许我们这样写\r\n\r\n```javascript\r\nconst store = createStore(reducer, rewriteCreateStoreFunc);\r\n```\r\n\r\n我们仅需要改下 createStore 函数，如果第二个参数是一个object，我们认为他是 initState，如果是 function，我们就认为他是 rewriteCreateStoreFunc。\r\n\r\n```javascript\r\nfunction craeteStore(reducer, initState, rewriteCreateStoreFunc){\r\n    if (typeof initState === 'function'){\r\n    rewriteCreateStoreFunc = initState;\r\n    initState = undefined;\r\n  }\r\n  ...\r\n}\r\n```\r\n\r\n#### 2 行代码的 replaceReducer\r\n\r\nreducer 拆分后，和组件是一一对应的。我们就希望在做按需加载的时候，reducer也可以跟着组件在必要的时候再加载，然后用新的 reducer 替换老的 reducer。\r\n\r\n```javascript\r\nconst createStore = function (reducer, initState) {\r\n  ...\r\n  function replaceReducer(nextReducer) {\r\n    reducer = nextReducer\r\n    /*刷新一遍 state 的值，新来的 reducer 把自己的默认状态放到 state 树上去*/\r\n    dispatch({ type: Symbol() })\r\n  }\r\n  ...\r\n  return {\r\n    ...\r\n    replaceReducer\r\n  }\r\n}\r\n```\r\n\r\n我们来尝试使用下\r\n\r\n```javascript\r\nconst reducer = combineReducers({\r\n  counter: counterReducer\r\n});\r\nconst store = createStore(reducer);\r\n\r\n/*生成新的reducer*/\r\nconst nextReducer = combineReducers({\r\n  counter: counterReducer,\r\n  info: infoReducer\r\n});\r\n/*replaceReducer*/\r\nstore.replaceReducer(nextReducer);\r\n```\r\n\r\nreplaceReducer 示例源码见 [demo-5](https://github.com/frontend9/redux-demo/tree/master/demo-5)\r\n\r\n#### bindActionCreators\r\n\r\nbindActionCreators 我们很少很少用到，一般只有在 react-redux 的 connect 实现中用到。\r\n\r\n他是做什么的？他通过闭包，把 dispatch 和 actionCreator 隐藏起来，让其他地方感知不到 redux 的存在。\r\n\r\n我们通过普通的方式来 隐藏 dispatch 和 actionCreator 试试，注意最后两行代码\r\n\r\n```javascript\r\nconst reducer = combineReducers({\r\n  counter: counterReducer,\r\n  info: infoReducer\r\n});\r\nconst store = createStore(reducer);\r\n\r\n/*返回 action 的函数就叫 actionCreator*/\r\nfunction increment() {\r\n  return {\r\n    type: 'INCREMENT'\r\n  }\r\n}\r\n\r\nfunction setName(name) {\r\n  return {\r\n    type: 'SET_NAME',\r\n    name: name\r\n  }\r\n}\r\n\r\nconst actions = {\r\n  increment: function () {\r\n    return store.dispatch(increment.apply(this, arguments))\r\n  },\r\n  setName: function () {\r\n    return store.dispatch(setName.apply(this, arguments))\r\n  }\r\n}\r\n/*注意：我们可以把 actions 传到任何地方去*/\r\n/*其他地方在实现自增的时候，根本不知道 dispatch，actionCreator等细节*/\r\nactions.increment(); /*自增*/\r\nactions.setName('九部威武'); /*修改 info.name*/\r\n```\r\n\r\n我眼睛一看，这个 actions 生成的时候，好多公共代码，提取一下\r\n\r\n```javascript\r\nconst actions = bindActionCreators({ increment, setName }, store.dispatch);\r\n```\r\n\r\n来看一下 bindActionCreators 的源码，超级简单（就是生成了刚才的 actions）\r\n\r\n```javascript\r\n/*核心的代码在这里，通过闭包隐藏了 actionCreator 和 dispatch*/\r\nfunction bindActionCreator(actionCreator, dispatch) {\r\n  return function () {\r\n    return dispatch(actionCreator.apply(this, arguments))\r\n  }\r\n}\r\n\r\n/* actionCreators 必须是 function 或者 object */\r\nexport default function bindActionCreators(actionCreators, dispatch) {\r\n  if (typeof actionCreators === 'function') {\r\n    return bindActionCreator(actionCreators, dispatch)\r\n  }\r\n\r\n  if (typeof actionCreators !== 'object' || actionCreators === null) {\r\n    throw new Error()\r\n  }\r\n\r\n  const keys = Object.keys(actionCreators)\r\n  const boundActionCreators = {}\r\n  for (let i = 0; i < keys.length; i++) {\r\n    const key = keys[i]\r\n    const actionCreator = actionCreators[key]\r\n    if (typeof actionCreator === 'function') {\r\n      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)\r\n    }\r\n  }\r\n  return boundActionCreators\r\n}\r\n```\r\n\r\nbindActionCreators 示例源码见 [demo-8](https://github.com/frontend9/redux-demo/tree/master/demo-8)\r\n\r\n#### 大功告成\r\n\r\n完整的示例源码见 [demo-9](https://github.com/frontend9/redux-demo/tree/master/demo-9)，你可以和 [redux](https://github.com/reduxjs/redux/issues) 源码做一下对比，你会发现，我们已经实现了 redux 所有的功能了。\r\n\r\n当然，为了保证代码的理解性，我们少了一些参数验证。比如 `createStore(reducer)`的参数 reducer 必须是 function 等等。\r\n\r\n### <div id=\"practice\">最佳实践</div>\r\n\r\n#### 纯函数\r\n\r\n什么是纯函数？\r\n\r\n纯函数是这样一种函数，即相同的输入，永远会得到相同的输出，而且没有任何可观察的副作用。\r\n\r\n通俗来讲，就两个要素\r\n\r\n1. 相同的输入，一定会得到相同的输出\r\n2. 不会有 “触发事件”，更改输入参数，依赖外部参数，打印 log 等等副作用\r\n\r\n```javascript\r\n/*不是纯函数，因为同样的输入，输出结果不一致*/\r\nfunction a( count ){\r\n   return count + Math.random();\r\n}\r\n\r\n/*不是纯函数，因为外部的 arr 被修改了*/\r\nfunction b( arr ){\r\n    return arr.push(1);\r\n}\r\nlet arr = [1, 2, 3];\r\nb(arr);\r\nconsole.log(arr); //[1, 2, 3, 1]\r\n\r\n/*不是纯函数，以为依赖了外部的 x*/\r\nlet x = 1;\r\nfunction c( count ){\r\n    return count + x;\r\n}\r\n```\r\n\r\n我们的 reducer 计划函数，就必须是一个纯函数！\r\n\r\n**只要传入参数相同，返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用，没有 API 请求、没有变量修改，单纯执行计算。**\r\n\r\n### <div id=\"summary\">总结</div>\r\n\r\n到了最后，我想把 redux 中关键的名词列出来，你每个都知道是干啥的吗？\r\n\r\n- createStore\r\n\r\n  创建 store 对象，包含 getState, dispatch, subscribe, replaceReducer\r\n\r\n- reducer\r\n\r\n  reducer 是一个计划函数，接收旧的 state 和 action，生成新的 state\r\n\r\n- action\r\n\r\n  action 是一个对象，必须包含 type 字段\r\n\r\n- dispatch\r\n\r\n  `dispatch( action )` 触发 action，生成新的 state\r\n\r\n- subscribe\r\n\r\n  实现订阅功能，每次触发 dispatch 的时候，会执行订阅函数\r\n\r\n- combineReducers\r\n\r\n  多 reducer 合并成一个 reducer\r\n\r\n- replaceReducer\r\n\r\n  替换 reducer 函数\r\n\r\n- middleware\r\n\r\n  扩展 dispatch 函数！\r\n\r\n你再看 redux 流程图，是不是大彻大悟了？\r\n\r\n![redux 流程图](https://user-images.githubusercontent.com/12526493/48312444-8ff2e100-e5e9-11e8-844a-48ffd9933265.png)\r\n\r\n<center>(redux 流程图)</center>"},"comment":{"url":"https://api.github.com/repos/brickspert/blog/issues/comments/513142531","html_url":"https://github.com/brickspert/blog/issues/22#issuecomment-513142531","issue_url":"https://api.github.com/repos/brickspert/blog/issues/22","id":513142531,"node_id":"MDEyOklzc3VlQ29tbWVudDUxMzE0MjUzMQ==","user":{"login":"xingyongchao","id":11263058,"node_id":"MDQ6VXNlcjExMjYzMDU4","avatar_url":"https://avatars1.githubusercontent.com/u/11263058?v=4","gravatar_id":"","url":"https://api.github.com/users/xingyongchao","html_url":"https://github.com/xingyongchao","followers_url":"https://api.github.com/users/xingyongchao/followers","following_url":"https://api.github.com/users/xingyongchao/following{/other_user}","gists_url":"https://api.github.com/users/xingyongchao/gists{/gist_id}","starred_url":"https://api.github.com/users/xingyongchao/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/xingyongchao/subscriptions","organizations_url":"https://api.github.com/users/xingyongchao/orgs","repos_url":"https://api.github.com/users/xingyongchao/repos","events_url":"https://api.github.com/users/xingyongchao/events{/privacy}","received_events_url":"https://api.github.com/users/xingyongchao/received_events","type":"User","site_admin":false},"created_at":"2019-07-19T08:28:58Z","updated_at":"2019-07-19T08:28:58Z","author_association":"NONE","body":"通过简述过程中把redux源码解析出来， 大神666"}}
other: {"actor":{"display_login":"xingyongchao"}}