Flux 概述

Flux 是 Facebook 用来构建用户端的 Web 应用程序的体系架构,与其它形式化的框架相比,它更像是一个架构思想,用于管理和控制应用中数据的流向。这里应用中的数据指包括但不限于来自服务端的数据页面中 view 的一些状态(如一个面板是展开还是关闭),临时存储在本地需要持久化到服务端的数据等。

好了,说了这么多好像还是一脸懵逼,不慌,接下来看看展开式。

MVC

在讲述Flux之前,我们看看之前传统的 MVC 架构以及在前端中的一些问题继而思考 Flux 带来的改变。MVC(Model-View-Controller)最先兴起于后端,通过对应用程序复杂度的简化使程序更加直观和便于维护。后端程序 MVC 中 View 可以看为数据的呈现,Model 为数据的模型,Controller 作为程序的流程控制。现在假设有这样的场景,用户想查看自己的 profile 页面,可能会有这样的流程:在页面上点击 profile 按钮,接下来就是一个 HTTP 请求(/profile?username=jiavan) => Controller 接收到这一请求并获得请求的内容 username=jiavan 然后告知 Model 需要 jiavan 的数据 => Model 返回了 jiavan 的数据 => Controller 得到数据返回新的视图,看下流程:

现在前端中又有这样的场景:切换 Menu 中的 Item,当前选中的 Item 颜色不同于其它颜色并且底部显示对应 Item 的内容。一般情况下我们会定义一个 css class 来作为当前选中 Item 的样式。当用户点击 Item_A 为被点击的元素新增高亮的 class,其它兄弟元素移除该样式,这里的事件响应函数就是 Controller,我们会在这里处理样式修改逻辑,以及更新 Model 的数据,然后新的数据及样式重新渲染界面。这种VC<->M的形式在关系比较简单的情况下是比较清晰容易控制的,但是复杂的页面上这样的模式可能会变得非常混乱:

之所以变得混乱了,因为很多 view 都具备修改多个 model 的能力,这里的单个修改行为可以称之为一个 Action,一个 Action 的产生可能是用户行为,或者一个 Ajax 请求需要渲染新界面。对比上面后端传统 MVC 模式可以发现:

  • 后端中 Action 作为一个 URL 请求,前端中可能是一个事件;
  • 后端中 Action 处理被集中在 Controller 中,而前端中是分散的。

那么是不是可以把前端中修改状态即 state 的行为(事件回调/Ajax)全部抽象成一种 Action 描述,然后交付到一处即 Reducers 来进行原子化处理,然后 Reducer 修改整个应用中唯一的一棵 state tree 即 Store,最后通过 state->view 的机制来重新渲染?

Flux 数据流框架

上面提到的几个概念已经对 Flux 有了初步的了解,下面进入正题。相信有了解 Flux 的都应该看过下面这张著名的数据流图:

  1. Action 可以看成是修改 Store 的行为抽象;
  2. Dispatcher 管理着应用的数据流,可以看为 Action 到 Store 的分发器;
  3. Store 管理着整个应用的状态和逻辑,类似 MVC 中的 Model。

所以 Flux 可以被看作传统 MVC 的改进而非颠覆,当我第一次看到 Flux 的时候其实是比较懵逼,但看到并使用了 Redux 后确实有一种非常惊艳的感觉。

Redux

按照 Redux 官方的描述Redux is a predictable state container for JavaScript apps.,其中predictablestate container体现了它的作用。那么如何来理解可预测化的呢?这里会有一些函数式编程方面的思想,在 Redux 中 reducer 函数是一个纯函数,相同输入一定会是一致的输出,所以确定输入的 state 那么 reducer 函数输出的 state 一定是可以被预测的,因为它只会进行单纯的计算,保证正确的输出。状态容器又是什么?说明 Redux 有一个专门管理 state 的地方,就是 Store,并且一般情况下是唯一的,应用中所有 state 形成的一颗状态树就是 Store。Redux 由 Flux 演变而来,但受 Elm 的启发,避开了 Flux 的复杂性,我们看看其数据流向:

不同于 Flux 架构,Redux 中没有 dispatcher 这个概念,并且 Redux 设想你永远不会变动你的数据,你应该在 reducer 中返回新的对象来作为应用的新状态。但是它们都可以用(state, action) => newState来表述其核心思想,所以 Redux 可以被看成是 Flux 思想的一种实现,但是在细节上会有一些差异。

重要概念

  1. 应用中的所有 state 都以一个object tree的形式存储在一个单一的 store 中;
  2. 唯一能改 store 的方法是触发 action,action 是动作行为的抽象
  3. 为了描述 action 如何改变 state 树,需要编写 reducer 函数。

这里需要说明一点的是 reducer 函数,它应当是一个纯函数,不应该有副作用,不应有 API 调用,Date.now()或者随机获取等不稳定的操作,应当保证相同的输入 reducer 计算的结果应该是一致的输出,它只会进行单纯的计算。编写 reducer 函数也是 Redux 中比较重要的一块,它的形式如下:

function testReducer(state, action) {
  switch (action.type) {
    case ACTION_TYPE:
      // calc...
      return newState;
    default:
      return state;
  }
  return newState;
}

state 是不可修改的,所以返回的新 state 应该是基于输入 state 副本的修改,而不是直接修改 state 后的返回。

原则

1. 单一数据源,store

整个应用的 state 被存放在一棵 Object tree 中,并且这个 Object tree 只存在唯一一个 store 中;

2. state 是只读的

唯一能改变 state 的方法是触发 action,action 是对已经发生了的事情的抽象描述,简单的讲,它把行为抽象成了一个对象。

比如,删除一条记录的 action 可以抽象的理解为:

{
  type: 'DELETE_ITEM',
  index: 1,
}

3. 使用纯函数来实现 state 归并操作,reducer

传入待修改的 state 和一个告知 reducer 如何修改 state 的 action,reducer 将返回 action 规则对应下操作后的新的 state。

reducer(state, action) => new state

数据流

严格的单向数据流是Redux设计的核心
Redux 应用数据的生命周期遵循下面 4 个步骤: 0. 调用 store.dispatch(action), 可以在任何地方进行;

  1. Redux store 调用传入的 reducer 函数,并且将当前的 state 树与 action 传入。reducer 是纯函数,只用于计算下一个 state,它应该是完全可被预测的,相同的输入必定会有相同的输出,不能有副作用的操作,如 API 的调用或者路由跳转,这些应该都是在 dispatch 前产生;
  2. 根 reducer 将多个子 reducer 输出合并成一个单一的 state 树;
  3. Redux store 保存了根 reducer 返回的完整的 state 树。
    新的state树就是应用的下一个状态,现在就可以根据新的 state tree 来渲染 UI。

Redux 实践

我们通过一个非常简单的计数器 demo 来梳理 Redux 的数据流。

0x00. 创建 action

action 其实就是一个普通的对象,只是对行为的抽象描述,这里我们可以把加上一个数描述为:

{
  type: INCREMENT, //该动作的抽象描述
  number, // 该动作携带的数据
}

更多的时候我们会通过一个 action 生成函数来得到一个 action:

function incrementCreator(number) {
  return {
    type: INCREMENT,
    number,
  };
}

0x01. 创建 reducer 函数

reducer 作为整个 Redux 中 action 的处理中枢,接收 state 与 action 并对此修改数据,返回应用的下一个状态。

function countReducer(state, action) {
  switch (action.type) {
    case INCREMENT:
      return Object.assign(
        {},
        {
          counter: state.counter + action.number,
        }
      );
    case DECREMENT:
      return Object.assign(
        {},
        {
          counter: state.counter - action.number,
        }
      );
    default:
      return state;
  }
}

注意:上面我们已经提到多次,state 是不可修改的,所以通过assign归并我们对数据的操作,返回的是 state 副本修改后的对象,并非直接修改了输入的 state。

0x02. 创建唯一 store

通过 Redux 中的 createStore 方法传入 reducer 函数来创建整个应用的 store。

const store = createStore(countReducer);

0x03. 修改 state

通过 store 的 dispatch 方法来发起一个 action。

store.dispatch(incrementCreator(5));
store.dispatch(decrementCreator(4));

完整 demo

import { createStore } from "redux";

// actions
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";

// actionCreator,可以视为创建action的语法糖
function incrementCreator(number) {
  return {
    type: INCREMENT,
    number,
  };
}

function decrementCreator(number) {
  return {
    type: DECREMENT,
    number,
  };
}

// 初始化state
const initialState = {
  counter: 0,
};

// reducers函数,注意最后一定要return state防止不能匹配到action的时候state丢失
function countReducer(state = initialState, action) {
  switch (action.type) {
    case INCREMENT:
      return Object.assign(
        {},
        {
          counter: state.counter + action.number,
        }
      );
    case DECREMENT:
      return Object.assign(
        {},
        {
          counter: state.counter - action.number,
        }
      );
    default:
      return state;
  }
}

// 创建store
const store = createStore(countReducer);

// 订阅store的修改
const unsubscribe = store.subscribe(function log() {
  console.log(store.getState());
});

// 通过dispatch action来改变state
store.dispatch(incrementCreator(5)); //Object {counter: 5}
store.dispatch(decrementCreator(4)); //Object {counter: 1}

// 取消订阅
unsubscribe();

##参考并推荐阅读

原文链接 http://jiavan.com/flux-and-redux/ 转载请注明出处。