设计原则

Redux 设计理念与基本使用

Redux 致力于打造一个可预测的(Predicted State)状态容器,那么就需要将应用中所有的 State 以一个对象树的形式存储在一个单一的 Store 中。而唯一可以改变 State 的方法就是触发 Action,Action 即是用于描述发生了什么的一个对象。而实际上根据不同的 Action 而执行不同的对于 State 的操作则由 Reducer 来完成。整个 Redux 各个组件之间的数据流关系如下所示:

一个最简单的 Redux 的各部分的组合如下所示:

import { createStore } from "redux";

/**
 * 这是一个 reducer,形式为 (state, action) => state 的纯函数。
 * 描述了 action 如何把 state 转变成下一个 state。
 *
 * state 的形式取决于你,可以是基本类型、数组、对象、
 * 甚至是 Immutable.js 生成的数据结构。惟一的要点是
 * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
 *
 * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper)
 * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
    case "INCREMENT":
      return state + 1;
    case "DECREMENT":
      return state - 1;
    default:
      return state;
  }
}

// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);

// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() => console.log(store.getState()));

// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({ type: "INCREMENT" });
// 1
store.dispatch({ type: "INCREMENT" });
// 2
store.dispatch({ type: "DECREMENT" });
// 1

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。这让同构应用开发变得非常容易。来自服务端的 state 可以在无需编写更多代码的情况下被序列化并注入到客户端中。由于是单一的 state tree,调试也变得非常容易。在开发中,你可以把应用的 state 保存在本地,从而加快开发速度。此外,受益于单一的 state tree,以前难以实现的如“撤销/重做”这类功能也变得轻而易举。

console.log(store.getState());

{
  visibilityFilter: 'SHOW_ALL',
  todos: [{
    text: 'Consider using Redux',
    completed: true,
  }, {
    text: 'Keep all state in a single tree',
    completed: false
  }]
}

Redux 与 Flux

Redux 与 Flux 对比

Redux 的灵感来源于 Flux 的几个重要特性。和 Flux 一样,Redux 规定,将模型的更新逻辑全部集中于一个特定的层(Flux 里的 store,Redux 里的 reducer)。Flux 和 Redux 都不允许程序直接修改数据,而是用一个叫作 “action” 的普通对象来对更改进行描述。

而不同于 Flux,Redux 并没有 dispatcher 的概念。原因是它依赖纯函数来替代事件处理器。纯函数构建简单,也不需额外的实体来管理它们。你可以将这点看作这两个框架的差异或细节实现,取决于你怎么看 Flux。Flux 常常被表述为 (state, action) => state。从这个意义上说,Redux 无疑是 Flux 架构的实现,且得益于纯函数而更为简单。

和 Flux 的另一个重要区别,是 Redux 设想你永远不会变动你的数据。你可以很好地使用普通对象和数组来管理 state,而不是在多个 reducer 里变动数据。正确且简便的方式是,你应该在 reducer 中返回一个新对象来更新 state,同时配合 object spread 运算符提案 或一些库,如 Immutable。

虽然出于性能方面的考虑,写不纯的 reducer 来变动数据在技术上是可行的,但我们并不鼓励这么做。不纯的 reducer 会使一些开发特性,如时间旅行、记录/回放或热加载不可实现。此外,在大部分实际应用中,这种数据不可变动的特性并不会带来性能问题,就像 Om 所表现的,即使对象分配失败,仍可以防止昂贵的重渲染和重计算。而得益于 reducer 的纯度,应用内的变化更是一目了然。

State 是只读的

惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。这样确保了视图和网络请求都不能直接修改 state,相反它们只能表达想要修改的意图。因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心 race condition 的出现。Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。

store.dispatch({
  type: "COMPLETE_TODO",
  index: 1
});

store.dispatch({
  type: "SET_VISIBILITY_FILTER",
  filter: "SHOW_COMPLETED"
});

使用纯函数来执行修改

为了描述 action 如何改变 state tree,你需要编写 reducers。Reducer 只是一些纯函数,它接收先前的 state 和 action,并返回新的 state。刚开始你可以只有一个 reducer,随着应用变大,你可以把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,你可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器。

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 "COMPLETE_TODO":
      return [
        ...state.slice(0, action.index),
        Object.assign({}, state[action.index], {
          completed: true
        }),
        ...state.slice(action.index + 1)
      ];
    default:
      return state;
  }
}

import { combineReducers, createStore } from "redux";
let reducer = combineReducers({ visibilityFilter, todos });
let store = createStore(reducer);

就是这样,现在你应该明白 Redux 是怎么回事了。

Unidirectional Data Flow(单向数据流)

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

或许你不需要 Redux

Redux 已经成为了前端状态管理的首选阵容之一,有点政治正确的说,很多人在他们真的需要用到 Redux 之前就毫不犹豫的加入了 Redux,从架构师的角度会觉得不用 Redux 以后怎么才能保证程序的可扩展性呢?而从底层码农的角度来看,他们会觉得喵了个咪的我为了实现一个简单的功能却要添加三个文件,好麻烦。作为 React 与 Redux 的核心开发人员之一,我能理解很多人经常抱怨 Redux、React、Functional Programming、Immutability 这些概念实在是学习的有些陡峭。与像 Mobx 这样同样优秀的状态管理框架,它们并不需要你去写大量的模板代码然后来更新状态。如果你打算使用 Redux,你在享受其带来的好处的同时也要遵守以下准则:

  • 必须使用基本对象与数组来描述应用状态
  • 必须使用基本的对象来描述系统变化
  • 必须使用纯函数来处理系统中的业务逻辑

实际上,在没有 Redux 的年代里,我们在构建 WebAPP 的时候并不需要这些准则的束缚,同样的,用不用 React 都可以。因此,我还是建议在你打算引入 React 或者 Redux 之前深思熟虑一下:

如果你正在构建一个可扩展的命令行工具JavaScript 调试工具或者类似于这样的 WebAPPs,那么我是很推荐你考虑将 Redux 引入到项目生命周期中的,或者说很推荐你去尝试下 Redux 中的一些思想。不过如果你是才开始学习 React,那么你也毋庸着急地去踏入 Redux 的世界。就好像 Think in React 中所述,如果你已经有了坚实的理由来使用 Redux 或者你本来就是打算尝试下新东西,那么你应该尝试下 Redux。另一方面,如果你本身在接触 Redux 的时候感觉压力满满,或者说你的同事们并不是那么喜欢 Redux,那么还是要再慎重考虑下。

最后,我必须要强调的是,Redux 真正的灵魂在于其设计思想,你很多时候并不一定需要去用 Redux 库本身,你可以尝试着去应用它的思想:

import React, { Component } from 'react';

class Counter extends Component {
  state = { value: 0 };

  increment = () => {
    this.setState(prevState => ({
      value: prevState.value + 1
    }));
  };

  decrement = () => {
    this.setState(prevState => ({
      value: prevState.value - 1
    }));
  };

  render() {
    return (
      <div>
        {this.state.value}
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
      </div>
    )
  }

注意,Local State is Fine,不要不分青红皂白地就否定掉 Local State,如果我们对上面的代码做进一步改造的话:

import React, { Component } from "react";

const counter = (state = { value: 0 }, action) => {
  switch (action.type) {
    case "INCREMENT":
      return { value: state.value + 1 };
    case "DECREMENT":
      return { value: state.value - 1 };
    default:
      return state;
  }
};

class Counter extends Component {
  state = counter(undefined, {});

  dispatch(action) {
    this.setState(prevState => counter(prevState, action));
  }

  increment = () => {
    this.dispatch({ type: "INCREMENT" });
  };

  decrement = () => {
    this.dispatch({ type: "DECREMENT" });
  };

  render() {
    return (
      <div>
        {this.state.value}
        <button onClick={this.increment}>+</button>
        <button onClick={this.decrement}>-</button>
      </div>
    );
  }
}

Redux Library本身只是为了方便将 Reducer 挂载到单一的全局状态库中,你也可以用自己的方式来构建属于你的 Redux。

上一页
下一页