landv / landv.github.io

landv-blogs
https://landv.cn
2 stars 0 forks source link

[OHIF-Viewers]医疗数字阅片-医学影像-Module: Panel-自定义面板-中二-Redux&react-redux状态管理详解 #31

Open landv opened 3 years ago

landv commented 3 years ago

https://www.cnblogs.com/landv/p/13334759.html 阅读目录(Content)

引言

回到顶部(go to top)

[OHIF-Viewers]医疗数字阅片-医学影像-Module: Panel-自定义面板-中二-Redux&react-redux状态管理详解

先上官方文档,推荐阅读英文原版文档,是最新、最全、最官方的文档。

redux:

英文原版 https://redux.js.org/ 中文 https://www.redux.org.cn/

英文原版 https://react-redux.js.org/ 中文 https://hardocs.com/d/redux/react-redux/

React:负责组件的UI界面渲染; Redux:数据处理中心; React-Redux:连接组件和数据中心,也就是把React和Redux联系起来。

要注意的是,Redux 和 React-redux 并不是同一个东西。Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 React 和 Vue,甚至跟 jQuery 结合都没有问题。而 React-redux 就是把 Redux 这种架构模式和 React.js 结合起来的一个库,就是 Redux 架构在 React.js 中的体现。

回到顶部(go to top)

对React、Redux、React-Redux详细剖析

Redux

Redux是一种架构模式,是由flux发展而来的。

Redux三大原则

  1. 唯一数据源
  2. 状态只读
  3. 数据改变只能通过纯函数(reducer)完成

Redux核心api

Redux主要由三部分组成:store,reducer,action。

store

Redux的核心是store,它由Redux提供的 createStore(reducer, defaultState) 这个方法生成,生成三个方法,getState(),dispatch(),subscrible()。

reducer

reducer是一个纯函数,它根据previousState和action计算出新的state。 reducer(previousState,action)

action

action本质上是一个JavaScript对象,其中必须包含一个type字段来表示将要执行的动作,其他的字段都可以根据需求来自定义。

const ADD_TODO = 'ADD_TODO'
复制代码
{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

整合

他们三者之间的交互,可以由下图概括:

img

React-Redux

Redux 本身和React没有关系,只是数据处理中心,是React-Redux让他们联系在一起。

React-rRedux提供两个方法:connect和Provider。

connect

connect连接React组件和Redux store。connect实际上是一个高阶函数,返回一个新的已与 Redux store 连接的组件类。

const VisibleTodoList = connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

TodoList是 UI 组件,VisibleTodoList就是由 react-redux 通过connect方法自动生成的容器组件。

  1. mapStateToProps:从Redux状态树中提取需要的部分作为props传递给当前的组件。
  2. mapDispatchToProps:将需要绑定的响应事件(action)作为props传递到组件上。

img

Provider

Provider实现store的全局访问,将store传给每个组件。

原理:使用React的context,context可以实现跨组件之间的传递。

总结

下图阐述了它们三者之间的工作流程:

img

回到顶部(go to top)

Redux

下载示例跟着博文一起学习Redux

//先git clone https://github.com/reduxjs/redux.git
//cd examples/todos
//npm i
//npm start
组件 为什么先说这个组件呢,因为它丫的是在app.js或者index.js开始要搞的东东。 React-Redux 提供`Provider`组件,可以让容器组件拿到`state`。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import React from 'react' import ReactDOM from 'react-dom' import { Provider } from 'react-redux' import store from './store' import App from './App' const rootElement = document.getElementById('root') ReactDOM.render( , rootElement ) ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` 这句是重点 ``` ## 创建 Action [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` let nextTodoId = 0 export const addTodo = text => ({ type: 'ADD_TODO', id: nextTodoId++, text }) export const setVisibilityFilter = filter => ({ type: 'SET_VISIBILITY_FILTER', filter }) export const toggleTodo = id => ({ type: 'TOGGLE_TODO', id }) export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 首先,让我们来给 action 下个定义。 Action 是把数据从应用(译者注:这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 [`store.dispatch()`](https://cn.redux.js.org/docs/api/Store.html#dispatch) 将 action 传到 store. Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 `type` 字段来表示将要执行的动作(type: 'ADD_TODO')。多数情况下,`type` 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。 > ```js > import { ADD_TODO, REMOVE_TODO } from '../actionTypes' > ``` > ##### 样板文件使用提醒 > > 使用单独的模块或文件来定义 action type 常量并不是必须的,甚至根本不需要定义。对于小应用来说,使用字符串做 action type 更方便些。不过,在大型应用中把它们显式地定义成常量还是利大于弊的。参照 [减少样板代码](https://cn.redux.js.org/docs/recipes/ReducingBoilerplate.html) 获取更多保持代码简洁的实践经验。 除了 `type` 字段外,action 对象的结构完全由你自己决定。参照 [Flux 标准 Action](https://github.com/acdlite/flux-standard-action) 获取关于如何构造 action 的建议。 这时,我们还需要再添加一个 action index 来表示用户完成任务的动作序列号。因为数据是存放在数组中的,所以我们通过下标 `index` 来引用特定的任务。而实际项目中一般会在新建数据的时候生成唯一的 ID 作为数据的引用标识。 ``` { type: TOGGLE_TODO, index: 5 } ``` > *OHIF-Viewers是定义的action type 常量* 我们应该尽量减少在 action 中传递的数据。比如上面的例子,传递 `index` 就比把整个任务对象传过去要好。 最后,再添加一个 action type 来表示当前的任务展示选项。 ``` { type: SET_VISIBILITY_FILTER, filter: SHOW_COMPLETED } ``` ## Action 创建函数 > 关于是用const定义还是写之前的function定义比较问题,请看此博文: > > [[OHIF-Viewers\]医疗数字阅片-医学影像-中间插播一下-es6-使用const加箭头函数声明函数相对于function声明函数有什么好处?](https://www.cnblogs.com/landv/p/13334816.html) Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。 在 Redux 中的 action 创建函数只是简单的返回一个 action: ``` function addTodo(text) { return { type: ADD_TODO, text } } export const addTodo = text => ({ type: 'ADD_TODO', id: nextTodoId++, text }) ``` *博主更喜欢使用 下面这种箭头函数* 这样做将使 action 创建函数更容易被移植和测试。 在 [传统的 Flux](http://facebook.github.io/flux) 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function addTodoWithDispatch(text) { const action = { type: ADD_TODO, text } dispatch(action) } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 不同的是,Redux 中只需把 action 创建函数的结果传给 `dispatch()` 方法即可发起一次 dispatch 过程。 ``` dispatch(addTodo(text)) dispatch(completeTodo(index)) ``` 或者创建一个 被绑定的 action 创建函数 来自动 dispatch: ``` const boundAddTodo = text => dispatch(addTodo(text)) const boundCompleteTodo = index => dispatch(completeTodo(index)) ``` 然后直接调用它们: ``` boundAddTodo(text); boundCompleteTodo(index); ``` **store 里能直接通过 [`store.dispatch()`](https://cn.redux.js.org/docs/api/Store.html#dispatch) 调用 `dispatch()` 方法,但是多数情况下你会使用 [react-redux](http://github.com/gaearon/react-redux) 提供的 `connect()` 帮助器来调用。[`bindActionCreators()`](https://cn.redux.js.org/docs/api/bindActionCreators.html) 可以自动把多个 action 创建函数 绑定到 `dispatch()` 方法上。** ***OHIF-Viewers就是采用的\**[`bindActionCreators()`](https://cn.redux.js.org/docs/api/bindActionCreators.html) 方式进行绑定的。\***** Action 创建函数也可以是异步非纯函数。你可以通过阅读 [高级教程](https://cn.redux.js.org/docs/advanced/) 中的 [异步 action](https://cn.redux.js.org/docs/advanced/AsyncActions.html)章节,学习如何处理 AJAX 响应和如何把 action 创建函数组合进异步控制流。因为基础教程中包含了阅读高级教程和异步 action 章节所需要的一些重要基础概念, 所以请在移步异步 action 之前, 务必先完成基础教程。 **源码** actions.js [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` /* * action 类型 */ export const ADD_TODO = 'ADD_TODO' export const TOGGLE_TODO = 'TOGGLE_TODO' export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER' /* * 其它的常量 */ export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE' } /* * action 创建函数 */ export function addTodo(text) { return { type: ADD_TODO, text } } export function toggleTodo(index) { return { type: TOGGLE_TODO, index } } export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) [回到顶部(go to top)](https://www.cnblogs.com/landv/p/13334759.html#_labelTop) # Reducer Reducers 指定了应用状态的变化如何响应 [actions](https://cn.redux.js.org/docs/basics/Actions.html) 并发送到 store 的,记住 actions 只是描述了*有事情发生了*这一事实,并没有描述应用如何更新 state。 ## 设计 State 结构 在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来? 以 todo 应用为例,需要保存两种不同的数据: - 当前选中的任务过滤条件; - 完整的任务列表。 通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` { visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) > ##### 处理 Reducer 关系时的注意事项 > > 开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 [normalizr](https://github.com/gaearon/normalizr) 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 `todosById: { id -> todo }` 和 `todos: array` 是比较好的方式,本文中为了保持示例简单没有这样处理。 ## Action 处理 现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。 ```js (previousState, action) => newState ``` 之所以将这样的函数称之为reducer,是因为这种函数与被传入 [`Array.prototype.reduce(reducer, ?initialValue)`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作: - 修改传入参数; - 执行有副作用的操作,如 API 请求和路由跳转; - 调用非纯函数,如 `Date.now()` 或 `Math.random()`。 在[高级篇](https://www.redux.org.cn/docs/advanced/)里会介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。 明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 [action](https://www.redux.org.cn/docs/basics/Actions.html)。 我们将以指定 state 的初始状态作为开始。Redux 首次执行时,state 为 `undefined`,此时我们可借机设置并返回应用的初始 state。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import { VisibilityFilters } from './actions' const initialState = { visibilityFilter: VisibilityFilters.SHOW_ALL, todos: [] }; function todoApp(state, action) { if (typeof state === 'undefined') { return initialState } // 这里暂不处理任何 action, // 仅返回传入的 state。 return state } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 这里一个技巧是使用 [ES6 参数默认值语法](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/default_parameters) 来精简代码。 ``` function todoApp(state = initialState, action) { // 这里暂不处理任何 action, // 仅返回传入的 state。 return state } ``` 现在可以处理 `SET_VISIBILITY_FILTER`。需要做的只是改变 state 中的 `visibilityFilter`。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) default: return state } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 注意: 1. 不要修改 `state`。 使用 [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 新建了一个副本。不能这样使用 `Object.assign(state, { visibilityFilter: action.filter })`,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案[对象展开运算符](https://www.redux.org.cn/docs/recipes/UsingObjectSpreadOperator.html)的支持, 从而使用 `{ ...state, ...newState }` 达到相同的目的。 2. 在 `default` 情况下返回旧的 `state`。遇到未知的 action 时,一定要返回旧的 `state`。 > `Object.assign` 须知 > > [`Object.assign()`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,[Babel 插件](https://github.com/babel-plugins/babel-plugin-object-assign),或者使用其它库如 [`_.assign()`](https://lodash.com/docs#assign) 提供的帮助方法。 > > ##### `switch` 和样板代码须知 > > `switch` 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。 > > 很不幸到现在为止,还有很多人存在一个误区:根据文档中是否使用 `switch` 来决定是否使用它。如果你不喜欢 `switch`,完全可以自定义一个 `createReducer` 函数来接收一个事件处理函数列表,参照["减少样板代码"](https://www.redux.org.cn/docs/recipes/ReducingBoilerplate.html#reducers)。 ## 处理多个 action 还有两个 action 需要处理。就像我们处理 `SET_VISIBILITY_FILTER` 一样,我们引入 `ADD_TODO` 和 `TOGGLE_TODO` 两个actions 并且扩展我们的 reducer 去处理 `ADD_TODO`. [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' ... function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) default: return state } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 如上,不直接修改 `state` 中的字段,而是返回新对象。新的 `todos` 对象就相当于旧的 `todos` 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。 最后,`TOGGLE_TODO` 的实现也很好理解: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) }) ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。(译者注:Javascript中的对象存储时均是由值和指向值的引用两个部分构成。此处突变指直接修改引用所指向的值, 而引用本身保持不变。) 如果经常需要这类的操作,可以选择使用帮助类 [React-addons-update](https://facebook.github.io/react/docs/update.html),[updeep](https://github.com/substantial/updeep),或者使用原生支持深度更新的库 [Immutable](http://facebook.github.io/immutable-js/)。最后,时刻谨记永远不要在克隆 `state` 前修改它。 ## 拆分 Reducer 目前的代码看起来有些冗长: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: [ ...state.todos, { text: action.text, completed: false } ] }) case TOGGLE_TODO: return Object.assign({}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) }) default: return state } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 上面代码能否变得更通俗易懂?这里的 `todos` 和 `visibilityFilter` 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 `todos` 更新的业务逻辑拆分到一个单独的函数里: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign({}, state, { visibilityFilter: action.filter }) case ADD_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) case TOGGLE_TODO: return Object.assign({}, state, { todos: todos(state.todos, action) }) default: return state } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 注意 `todos` 依旧接收 `state`,但它变成了一个数组!现在 `todoApp` 只把需要更新的一部分 state 传给 `todos` 函数,`todos` 函数自己确定如何更新这部分数据。这就是所谓的 *reducer 合成*,它是开发 Redux 应用最基础的模式。 下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 `visibilityFilter`?当然可以: 首先引用, 让我们使用 [ES6 对象结构](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) 去声明 `SHOW_ALL`: ``` const { SHOW_ALL } = VisibilityFilters ``` 接下来: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 `undefined`, 子 reducer 将负责返回它们的默认值。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 `state` 参数都不同,分别对应它管理的那部分 state 数据。 现在看起来好多了!随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。 最后,Redux 提供了 [`combineReducers()`](https://www.redux.org.cn/docs/api/combineReducers.html) 工具类来做上面 `todoApp` 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 `todoApp`: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import { combineReducers } from 'redux' const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 注意上面的写法和下面完全等价: ``` export default function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } } ``` 你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价: ``` const reducer = combineReducers({ a: doSomethingWithA, b: processB, c: c }) ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function reducer(state = {}, action) { return { a: doSomethingWithA(state.a, action), b: processB(state.b, action), c: c(state.c, action) } } ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。[没有任何魔法。](https://github.com/gaearon/redux/issues/428#issuecomment-129223274)正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。 > ##### ES6 用户使用注意 > > `combineReducers` 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 `export` 暴露出每个 reducer 函数,然后使用 `import * as reducers` 得到一个以它们名字作为 key 的 object: > > ```js > import { combineReducers } from 'redux' > import * as reducers from './reducers' > > const todoApp = combineReducers(reducers) > ``` > > 由于 `import *` 还是比较新的语法,为了避免[困惑](https://github.com/gaearon/redux/issues/428#issuecomment-129223274),我们不会在本文档中使用它。但在一些社区示例中你可能会遇到它们。 ## 源码 #### `reducers.js` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import { combineReducers } from 'redux' import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign({}, todo, { completed: !todo.completed }) } return todo }) default: return state } } const todoApp = combineReducers({ visibilityFilter, todos }) export default todoApp ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) [回到顶部(go to top)](https://www.cnblogs.com/landv/p/13334759.html#_labelTop) # Store 在前面的章节中,我们学会了使用 [action](https://www.redux.org.cn/docs/basics/Actions.html) 来描述“发生了什么”,和使用 [reducers](https://www.redux.org.cn/docs/basics/Reducers.html) 来根据 action 更新 state 的用法。 Store 就是把它们联系到一起的对象。Store 有以下职责: - 维持应用的 state; - 提供 [`getState()`](https://www.redux.org.cn/docs/api/Store.html#getState) 方法获取 state; - 提供 [`dispatch(action)`](https://www.redux.org.cn/docs/api/Store.html#dispatch) 方法更新 state; - 通过 [`subscribe(listener)`](https://www.redux.org.cn/docs/api/Store.html#subscribe) 注册监听器; - 通过 [`subscribe(listener)`](https://www.redux.org.cn/docs/api/Store.html#subscribe) 返回的函数注销监听器。 再次强调一下 Redux 应用只有一个单一的 store。当需要拆分数据处理逻辑时,你应该使用 [reducer 组合](https://www.redux.org.cn/docs/basics/Reducers.html#splitting-reducers) 而不是创建多个 store。 根据已有的 reducer 来创建 store 是非常容易的。在[前一个章节](https://www.redux.org.cn/docs/basics/Reducers.html)中,我们使用 [`combineReducers()`](https://www.redux.org.cn/docs/api/combineReducers.html) 将多个 reducer 合并成为一个。现在我们将其导入,并传递 [`createStore()`](https://www.redux.org.cn/docs/api/createStore.html)。 ``` import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp) ``` [`createStore()`](https://www.redux.org.cn/docs/api/createStore.html) 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。 ```js let store = createStore(todoApp, window.STATE_FROM_SERVER) ``` ## 发起 Actions 现在我们已经创建好了 store ,让我们来验证一下!虽然还没有界面,我们已经可以测试数据处理逻辑了。 [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions' // 打印初始状态 console.log(store.getState()) // 每次 state 更新时,打印日志 // 注意 subscribe() 返回一个函数用来注销监听器 const unsubscribe = store.subscribe(() => console.log(store.getState()) ) // 发起一系列 action store.dispatch(addTodo('Learn about actions')) store.dispatch(addTodo('Learn about reducers')) store.dispatch(addTodo('Learn about store')) store.dispatch(toggleTodo(0)) store.dispatch(toggleTodo(1)) store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED)) // 停止监听 state 更新 unsubscribe(); ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 可以看到 store 里的 state 是如何变化的: ![img](https://img2020.cnblogs.com/blog/372674/202007/372674-20200722163900190-1205441055.png) 可以看到,在还没有开发界面的时候,我们就可以定义程序的行为。而且这时候已经可以写 reducer 和 action 创建函数的测试。不需要模拟任何东西,因为它们都是[纯函数](https://www.redux.org.cn/docs/introduction/ThreePrinciples.html#使用纯函数来执行修改)。只需调用一下,对返回值做断言,写测试就是这么简单。 ## 源码 #### `index.js` ``` import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp) ``` [回到顶部(go to top)](https://www.cnblogs.com/landv/p/13334759.html#_labelTop) # 数据流 严格的单向数据流是 Redux 架构的设计核心。 这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。 如果这些理由还不足以令你信服,读一下 [动机](https://www.redux.org.cn/docs/introduction/Motivation.html) 和 [Flux 案例](https://medium.com/@dan_abramov/the-case-for-flux-379b7d1982c6),这里面有更加详细的单向数据流优势分析。虽然 Redux 并不是严格意义上的 [Flux](https://www.redux.org.cn/docs/introduction/Relation to Other Libraries.md),但它们有共同的设计思想。 Redux 应用中数据的生命周期遵循下面 4 个步骤: 1. 调用 [`store.dispatch(action)`](https://www.redux.org.cn/docs/api/Store.html#dispatch)。 [Action](https://www.redux.org.cn/docs/basics/Actions.html) 就是一个描述“发生了什么”的普通对象。比如: ``` { type: 'LIKE_ARTICLE', articleId: 42 } { type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } } { type: 'ADD_TODO', text: 'Read the Redux docs.' } ``` 2.可以把 action 理解成新闻的摘要。如 “玛丽喜欢42号文章。” 或者 “任务列表里添加了'学习 Redux 文档'”。 你可以在任何地方调用 [`store.dispatch(action)`](https://www.redux.org.cn/docs/api/Store.html#dispatch),包括组件中、XHR 回调中、甚至定时器中。 Redux store 调用传入的 reducer 函数。 [Store](https://www.redux.org.cn/docs/basics/Store.html) 会把两个参数传入 [reducer](https://www.redux.org.cn/docs/basics/Reducers.html): 当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` // 当前应用的 state(todos 列表和选中的过滤器) let previousState = { visibleTodoFilter: 'SHOW_ALL', todos: [ { text: 'Read the docs.', complete: false } ] } // 将要执行的 action(添加一个 todo) let action = { type: 'ADD_TODO', text: 'Understand the flow.' } // reducer 返回处理后的应用状态 let nextState = todoApp(previousState, action) ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 注意 reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。 3.根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。 根 reducer 的结构完全由你决定。Redux 原生提供[`combineReducers()`](https://www.redux.org.cn/docs/api/combineReducers.html)辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。 下面演示 [`combineReducers()`](https://www.redux.org.cn/docs/api/combineReducers.html) 如何使用。假如你有两个 reducer:一个是 todo 列表,另一个是当前选择的过滤器设置: [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) ``` function todos(state = [], action) { // 省略处理逻辑... return nextState } function visibleTodoFilter(state = 'SHOW_ALL', action) { // 省略处理逻辑... return nextState } let todoApp = combineReducers({ todos, visibleTodoFilter }) ``` [![复制代码](https://common.cnblogs.com/images/copycode.gif)](javascript:void(0);) 当你触发 action 后,`combineReducers` 返回的 `todoApp` 会负责调用两个 reducer: ``` let nextTodos = todos(state.todos, action) let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action) ``` 然后会把两个结果集合并成一个 state 树: ``` return { todos: nextTodos, visibleTodoFilter: nextVisibleTodoFilter } ``` 虽然 [`combineReducers()`](https://www.redux.org.cn/docs/api/combineReducers.html) 是一个很方便的辅助工具,你也可以选择不用;你可以自行实现自己的根 reducer! 4.Redux store 保存了根 reducer 返回的完整 state 树。 这个新的树就是应用的下一个 state!所有订阅 [`store.subscribe(listener)`](https://www.redux.org.cn/docs/api/Store.html#subscribe) 的监听器都将被调用;监听器里可以调用 [`store.getState()`](https://www.redux.org.cn/docs/api/Store.html#getState) 获得当前 state。 现在,可以应用新的 state 来更新 UI。如果你使用了 [React Redux](https://github.com/gaearon/react-redux) 这类的绑定库,这时就应该调用 `component.setState(newState)` 来更新。 部分参考内容: https://juejin.im/post/5b2e3b9451882574934c3c8d#heading-4 https://redux.js.org/ https://www.redux.org.cn/ https://react-redux.js.org/ https://hardocs.com/d/redux/react-redux/ https://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_three_react-redux.html https://cn.redux.js.org/docs/basics/ExampleTodoList.html