Skip to content
Zheng Ping edited this page Mar 22, 2016 · 2 revisions

Redux

Table of Contents

Back to TOC

特性

Redux由少量能协同工作的函数组成,它可以日志打印,热加载,时间旅行,同构应用,录制和重放,无需开发者携带任何其它东西。

Back to TOC

要点

你的程序的所有状态都以一个对象树的形式存储在单个store中。唯一改变状态树的方法是触发一个action,发出一个描述发生了什么的对象。为了描述action如何改变状态树,你需要编写reducers

你应该把要做的修改变成一个普通对象,这个对象被叫做action,而不是直接修改 state。然后编写专门的函数来决定每个action如何改变应用的state,这个函数被叫做reducer

如果你以前使用Flux,那么你只需要注意一个重要的区别。Redux没有Dispatcher且不支持多个store。相反,只有一个单一store和一个单一的根级reduce函数(reducer)。随着应用不断变大,你应该把根级的reducer拆成多个小的reducers,分别独立地操作state树的不同部分,而不是添加新的stores。这就像一个React应用只有一个根级的组件,这个根组件又由很多小组件构成。

用这个架构开发计数器有点杀鸡用牛刀,但它的威力在于做复杂应用和庞大系统时优秀的扩展能力。由于它可以追溯action所产生的每一次变更,因此才有强大的开发工具。如录制用户会话并回放所有action来重现它。

Back to TOC

三个原则

  • 单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

  • 状态只读

惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。整个 state tree 应被视为只读,并需通过 Redux 来更新 state 和订阅更新。

  • 用纯函数来进行修改

为了描述 action 如何改变状态树,你需要编写 reducers。Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。

技术概览

Flux 常常被表述为(state, action) => state。从这个意义上说,Redux 无疑是 Flux 架构的实现,且得益于纯函数而更为简单。不同于 Flux ,Redux 并没有 dispatcher 的概念。和 Flux 的另一个重要区别,是Redux 设想你永远不会变动你的数据。你可以很好地使用普通对象和数组来管理状态,而不是在多个 reducer 里变动数据,你应该在 reducer 中返回一个新对象来更新 state。

Redux 并不在意你如何存储 state,state 可以是普通对象,不可变对象,或者其它类型。注意,即便你使用支持cursor的不可变库,也不应在 Redux 的应用中使用。整个 state tree 应被视为只读,并需通过 Redux 来更新 state 和订阅更新。而如果只是想用 cursor 把 state tree 从 UI tree 解耦并逐步细化 cursor,应使用 selector 来替代。

基础

Action是把数据从应用送到store的信息承载体, 它描述有事情发生这一事实。它是store数据的唯一来源,一般来说你用store.dispatch()action发送到storeAction是普通的javascript对象,action 内使用一个字符串类型的type字段来表示将要执行的动作。多数情况下,type会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放action。除了type属性外,action对象的结构由你决定。我们应该尽量减少在action中传递数据,比如传递数据的索引就比把整个数据对象由action携带传递更好。

Action创建器(Action creator)实际是创建action的函数,redux中的Action创建器扮演生成器的角色,它只是简单的返回一个action对象。

dispatch()函数可以通过store.dispatch()直接访问,分发函数(dispatch)是触发改变状态的唯一方法。分发函数调用时,它会通过getState()得到当前的store状态一并和action传给reducerreducer的返回值会作为新的store的状态,发生变化的状态会被监听到并立即通知对应的处理句柄。但dispatch通常是你使用react-redux提供的connect()帮助器来调用。也可以用bindActionCreators()自动把多个action创建器绑定到一个dispatch()函数。bindActionCreators()的唯一用例是当你想把某个action creator传入某个没有意识到Redux的组件,并且你不想往这个组件中传入dispatchstorebindActionCreators()会返回模仿原生对象的对象,但它的每一个函数会马上分发(dispatch)由相应的action creator返回的action

Redux只有在reducer中不允许做有副作用的操作,一旦根(root)reducer返回新的状态,被订阅的监听器就会被调用,你可以在订阅的监听器里再次调用分发(dispatch)。这一点上ReduxFlux有点细微的不同。

Redux中,程序的所有状态都存放在一个对象里,建议写代码前先想一下这个对象的结构。程序状态尽可能不要包含UI相关的状态。

在一个复杂应用中,某些数据间可能会相互引用,建议你尽可能地把state范式化,不要出现嵌套。你可以把程序的state想像成数据库,每个数据以 ID 为主键,不同数据相互引用时通过 ID 来查找。

reducer就是一个接受旧的stateaction并返回一个新的state的函数。可以简单的表示为(previousState, action) => newState。简单的说,reducer根据action更新state

它之所以被称作reducer是因为它是一个会被传给Array.prototype.reduce(reducer, ?initialValue)的函数。所以它必须是**纯(pure)函数(对于纯的理解类似于函数式编程思想,即对于给定的输入必然返回固定的输出),并且在reducer不能:

  • 修改传入的参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now() 或 Math.random();

reducer中,它只计算下一个状态并返回它,没有特殊情况,没有副作用,没有API调用,没有参数修改。

Redux提供了一个combineReducers()工具可以把几个独立的reducer合并起来。combineReducers()所做的只是生成一个函数,这个函数来调用你的一系列reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后再把它们的结果合并到一个单独的对象中。

如果你使用ES6combineReducers接收一个对象,可以把所有顶级的reducer放到一个独立的文件中,通过export暴露出每个reducer函数,然后使用import * as reducers得到一个以它们名字作为 key 的 object,比如:

import { combineReducers } from 'redux';
import * as reducers from './reducers';

const todoApp = combineReducers(reducers);

store是一个把actionreducer连在一起的对象。store有如下职责:

  • 维持应用的state
  • 提供getState()方法获取 state;
  • 允许state通过dispatch(action)方法更新;
  • 通过subscribe(listener)注册监听器;
  • 通过调用被subscribe(listener)返回的函数取消刚才被注册的监听器;

一个Redux应用中只有一个store对象。如果你想切割你的数据处理逻辑,你应该在reducer上做文章。如果你使用react-redux或类似的绑定,你最好不要直接访问你的组件中的storestore的定义如下:

type store => {
  dispatch: Dispatch
  getState: () => State
  subscribe: (listener: () => void) => () => void
  replaceReducer: (reducer: Reducer) => void
}

当调用createStore()时,第二个可选的参数可以指定初始状态。

严格的单向数据流是Redux的核心,这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免拷贝多个独立的无法相互引用的数据。

Redux应用中数据的生命周期遵循下列四个步骤:

  1. 调用store.dispatch(action),可以在任何地方调用store.dispatch(action),包括组件中、XHR 回调中、甚至定时器中;
  2. Redux store调用你传入的reducer函数,Store会把两个参数传入 reducer: 当前的state树和action;
  3. reducer把多个子reducer的输出合并进一个单独的state树;
  4. Redux store会保存由根reducer返回的完整的状态树;

ReduxReact绑定遵循呈现组件和容器组件相分离的思想。呈现组件和容器组件又可以分别包含其它的呈现组件或容器组件。我们写的多数组件都是呈现组件,但我们也需要生成少量的容器组件来把它们连接到Redux store。虽然你可以通过store.subscribe()写出容器组件,但不建议你这么做,因为React Redux已经做了很多优化工作,建议你用React Reduxconnect()函数来生成容器组件。

如果容器组件要访问Redux store,它们可以使用订阅(subscribe)来实现。推荐使用一个专门的Redux组件<Provider>,它能使应用中的所有组件都能用到store而不用把store在程序中传来传去。你只需要在渲染根组件的时候用一次就可以了。

高级

异步action

当调用异步API时,有两个主要的时间点: 发起请求的时刻,和接收到响应的时刻(也可能是超时)。这两个时间点上可能都会改变应用的状态,一般对于任何的API请求,你可能需要分发至少三个不同类型的action:

  • 一个通知reducer请求开始的action

    对于这种action,reducer 可能会切换一下state中的isFetching标记。以此来告诉 UI 来显示进度条。

  • 一个通知reducer请求成功结束的action

    对于这种actionreducer可能会把接收到的新数据合并到state中,并重置 isFetching。UI 则会隐藏进度条,并显示接收到的数据。

  • 一个通知reducer请求失败的action

    对于这种actionreducer可能会重置isFetching。或者,有些reducer会保存这些失败信息,并在 UI 里显示出来。

面对异步问题时,首先要考虑的问题是状态形态(state shape),为了使用异步action,你可能要用到某些中间件。通过这个中间件,action创建器会返回一个函数而非对象,这个被返回的函数可以不用是纯(pure)函数,比如它可以执行某些异步API调用或分发某些action

如果不用中间件,Redux store只支持同步数据流,这是createStore创建的store的默认行为,你可以用applyMiddleware()来增强createStore(),它能让你方便的传递异步action。像redux-thunkredux-promise之类的异步中间件封装了storedispatch方法,除了action外它允许你分发其它的东西,比如函数或promise。你使用的任意中间件可以解析你分发的所有东西,并且可以把action按顺序传给下一个中间件。当中间件队列中的最后一个中间件分发一个action时,这个action必须是纯对象,这正是同步的Redux数据流发生的时候。

中间件

Redux的中间件是位于action被分发到达reducer的那个中间点。Redux的中间件可以用来做记录日志、创建崩溃报告、调用异步接口或路由等事情。中间件的关键特性是它是可组装的,多个中间件可以组合在一起,每个中间件无需了解它在中间件链条中的前后数据关系。

Redux的好处是它让状态的改变可预测并且透明化。中间件用了一种嵌套调用的思想。中间件让你能够封装storedispatch方法。

中间件可以表示为:

({getState, dispatch}) => next => action

applyMiddleware()会返回一个store enhancer。中间件本质上是一种扩展机制,applyMiddleware就是Redux强大的扩展机制的一个例子,中间件比store enhancer要弱一些,但是它更易于书写。中间件比它实际上听起来要复杂得多,最好的理解中间件的方法是看代码。如果想要试用多个store enhancer,你需要使用compose()

其他

你所写的多数Redux代码甚至都不用使用ReduxAPI,多数时候你都是在写函数。

Redux深蕴Flux store的本质,从Flux项目往Redux迁移是很容易的事情,反之同理。而且两者还可以同时存在。

Redux的一个原则是从不改变状态,你可能经常要用到Object.assign()来用新的值或更新的值来创建对象的拷贝。你也可以使用新版JS中的对象展开语法(object spread syntax),对象展开语法在组装复杂对象时非常有用。

Action是描述应用中发生了什么事情的纯对象(plain object),是唯一能表达要改变数据意图的东西。把action设计成你必须分发的纯对象是Redux的一个基础。Actiontype定义成字符串常量有助于项目的长期进展。Action creator在项目的开发阶段非常有用,它对于解耦合也很有益处。任何数据,不管是UI事件、网络回调或者是诸如websocket等资源最终都要以action分发。

中间件让我们能够写出更具表现力的,具备异步特性的action creator。它可以分发并解析纯对象(plain object)。中间件的定义如下:

type MiddlewareAPI = { dispatch: Dispatch, getState: () => State }
type Middleware = (api: MiddlewareAPI) => (next: Dispatch) => Dispatch

一个中间件是一个合成了一个分发函数(dispatch function)来返回一个新的分发函数的高阶(high-order)函数。它通常会把一个异步action返回到action中。

Redux reducer的API是(state, action) => action,并没有强迫你要怎样去写一个reducer。可以把reducer理解成一个状态的累加器,类似于Array.prototype.reduce()的概念。reducer中被累加的值是action,累加出的结果是state

如果要借助Redux在服务端渲染,Redux的唯一工作是为应用提供初始状态。

推荐用Mocha做为测试引擎。

Reselect是一个创建内存中可组合的选择器函数,Reselect选择器可以用来通过Redux store高效地计算派生出的数据。Reselect提供了一个createSelector()函数可以创建一个内存中的选择器。createSelector()的参数是一个选择器数组和一个转换函数(transform function)。如果Redux的状态树发生了改变,并且状态的改变导致选择器的值也发生了改变,那么Reselect就会用选择器的值做为参数调用转换函数,转换函数会返还一个结果。如果选择器的值与先前调用选择器时的值一样,它将会返回以前计算出的值而不会调用转换函数。一个Reselect选择器也可以做为另一个选择器中选择器数组参数中的元素。Reselect的选择器也可以连接到Redux Store

reducer enhancer是一个以reducer为参数,返回一个新的reducer的函数(类似于高阶函数的概念),这个被返回的函数可以处理新的actions,或持有更多的状态,或者分发它处理不了的action到内部的actioncombineReducers()就是一个reducer enhancer

redux-undo也许对于处理undo问题能提供便捷帮助。

分发函数的定义如下:

type BaseDispatch = (a: Action) => Action
type Dispatch = (a: Action | AsyncAction) => any

一个分发函数可能分发一个或多个actionstore中,也可能不分发actionstore实例的dispatch函数是一个基本的分发函数。基本分发函数(base dispatch)永远都是同步发送一个actionstorereducer,同时还会携带由store返回的当前状态,reducer将会计算出新的状态。基本分发函数期望action是一个纯对象。

中间件封装了基础分发函数,它可能会转换、延迟、忽略或者是解析异步action,然后再把它们传递给下一个中间件。

action creator的定义是type ActionCreator = (...args: any) => Action | AsyncAction,如果一个action creator需要读当前的状态、执行一个API、或者会产生某些副作用(比如路由过渡),那么它就应该返回一个异步action(async action)。一个异步action是一个被发送到一个分发函数的值,该值不会被reducer消费,在它被发送给基础分发函数前,它将会被中间件转换成一个action

store creator的定义如下:

type StoreCreator = (reducer: Reducer, initialState: ?State) => Store

类似于分发函数,也存在一个基础的store创建器createStore(reducer, initialState)store enhancerstore enhancer的定义如下:

type StoreEnhancer = (next: StoreCreator) => StoreCreator

一个store enhancer是一个组合一个store creator并返回一个新store creator的高阶函数。这有点类似于中间件。因为store是一个函数的纯对象集合,而不是某个实例,所有可以很方便地创建它的拷贝,并在这个拷贝上修改数据,这样不会影响到原始store。多数情况下你不需要写store enhancer,你可以直接使用Redux开发工具中提供的那一个。有意思的是Redux中间件的实现本身也是一个store enhancer

记住Redux本身只涉及到状态管理,在一个真实应用中你可能需要像react-redux之类的UI绑定。

compose()从右向左合成函数,它是一个函数式编程的工具,你可以把它用到一系列按行排列的store enhancer中。compose()会返回给定函数按从右向左合成后的函数。