状态管理

今天来学学 JavaScript 中的状态管理,现在有很多比较火的 state manager:

  • Flux
  • Redux
  • Vuex
  • MobX
  • Dva

本文章是对这几个状态管理库的总结。

Flux

flux-simple-f8-diagram-with-client-action-1300w.png

Flux 主要包含三个部分:Dispatcher(p 派发者),Stores(仓库) 和 Views(视图),看上去像是 MVC 那一套,但实际上 Flux 是用来替代 MVC 模式的一种思想(当然,facebook 以此开发了一个同名的 flux)。

  • View: Flux 的 View 不同于 MVC 的 View,它更像是 View 和 Controller 的结合,既能展示数据,又可以与用户进行交互处理请求(Action),MVC 对用户的交互请求会调用 Model 的业务逻辑处理接口,而 Flux 则会将用户操作映射为 Action,View 同时监听着 Store 中数据的更改,一旦发生更改会重新向 Store 请求数据。

  • Dispatcher: Dispatcher 相当于一个中心枢纽(central hub),所有 Actions 都会经过 Dispatcher 进行处理,Dispatcher 会调用 Store 注册在 Action 上的回调函数,注意:Dispatcher 本质上就是一个回调注册表,本身不包含任何的业务逻辑

随着应用程序的发展,dispatcher 变得越发重要,因为它可以按照特定的顺序调用注册的回调来管理 Store 中的数据,可以等待其他 Store 中的数据完成更新再更新自己的数据。

  • Stores: 仓库中包含数据和业务逻辑,但不同于 MVC 的 Model,Store 中可能包含许多个对象,而一个 Model 只对应一个对象,而且:只有 Store 自己知道如何修改数据,不会对外提供直接修改数据的接口

了解了 Flux 的结构,可以清楚地知道:Flux 是单项数据流

为什么使用单项数据流

  1. 数据的双向绑定可能会导致级联更新,就是说更改一个值可能会导致另一个值的更改从而可能会影响到其他值的改变,这使得用户交互产生的结果难以预测,使用单项数据流不会有这个问题。
  2. 方便追踪变化,所有引起数据变化的原因都可由 Action 进行描述,而 Action 只是一个纯对象,因此十分易于序列化或查看。
  3. 集中化管理数据。常规应用可能会在视图层的任何地方或回调进行数据状态的修改与存储,而在 Flux 架构中,所有数据都只放在 Store 中进行储存与管理。

为什么要使用 Flux 代替 MVC

MVC(Model,View,Controller)架构中三个部份是 1:1:1 的关系,这只是一种理想状态,现实中的程序往往是多视图,多模型,视图和模型之间也可以是多对多的关系,这种情况下使用 MVC 会造成许多问题,下图就是混乱的 MVC。

ff33122943c99967d5809725e10b2f7e_r.png

MVC 是双向数据流架构,我们在前面提到过双向数据流的缺陷:导致数据的级联更新,这会使调试变得非常困难,因为有许多种情况可以改变需要调试的数据,判断出问题的根源需要很多时间。

相比之下,Flux 会使结构简单得多:

5b3b3765741c4735792e9824be265835_720w.png

Flux 的特点

  • 单项数据流,用户操作视图还有组件初始化等情况的时候发出 Action,交给 Dispatcher 派发给 Store,Store 执行注册在 Action 上的回调函数以更新数据。
  • Store 和 View 是多对多的关系。
  • Store 中存放了数据和业务逻辑。
  • Store 没有对外暴露直接修改数据的接口,但有直接获取数据的接口。

Redux

redux-data-flow.png

Redux 由三部分组成:Store(仓库),Views(视图)和 Reducers(派发者)。

  • Store: Store 是把 Reducers 和 Action 联系到一起的对象,提供获取,更新 State 的方法,也提供了 subscribe 注册和注销监听器,Store 是单一的,当需要拆分数据处理逻辑时应该调用多个 Reducer 而不是创建多个 Store。
  • Views: 同 Flux 相同,用户通过 View 触发改变数据。
  • Reducers: 指定了应用状态的变化如何响应 actions 并发送到 Store 的。

Redux 的设计灵感来源于 Flux,实际上两者非常相似,但也有些不同。

1607246729_1_.png

  • Redux 没有 Dispatcher 这个概念,它依赖纯函数open in new window来替代事件处理器,纯函数的构建简单,管理容易。
  • Redux 假定数据永远不会改变,所以可以很好地使用普通对象和数组管理 state,在每次 Action 请求触发后,Redux 都会生成一个新的对象来更新 state,而不是在当前状态上进行修改。
  • Redux 只有一个 Store,存储了整个应用程序的 State。
  • Action Creator,在 Flux 中,Action Creator 在创建 Action 的同时会触发 Dispatch 操作,而 Redux 的 Creator 只负责创建,这也使得 Action Creator 的行为变得简单也便于测试。

Redux 对于异步 Action,提供了 Middleware(中间件),这是对 store.dispatch() 的进一步封装,使 dispatch 可以传递除了 Action 之外的函数或者 promise,目前比较流行的有 Redux-thunk 和 Redux-promise。

Redux 的使用

用官网的例子来看看 Redux 是如何使用的:

Action.js

// type
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_COMPLETED, SHOW_ACTIVE }
// actions
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 }
}

state

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

注意:

开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。

Reducers.js

import { combineReducers } from 'redux'
import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions'
const { SHOW_ALL } = VisibilityFilters
/**
 * visibilityFilter 和 todos 是处理 action 的 reducer 函数,每个 action 都有一个对应的 type,通过 type 值判断修改哪个 state
 * 由于 visibilityFilter 和 todos 的更新是相互独立的,所以我们使用两个 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:
      // 添加新的 todos item
      return [
        ...state,
        {
          text: action.text,
          completed: false,
        },
      ]
    case TOGGLE_TODO:
      // 遍历 Store 中的 todos 数组,更改对应 item 的 completed 值
      return state.map((todo, index) => {
        if (index === action.index) {
          // 不能直接修改原本的 state,而是生成一个新的 state
          return Object.assign({}, todo, {
            completed: !todo.completed,
          })
          // 等同于 return {...todo, completed: !todo.completed}
        }
        return todo
      })
    default:
      return state
  }
}
// combineReducers 生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象
const todoApp = combineReducers({
  visibilityFilter,
  todos,
})
// 注意上面的写法和下面完全等价:
// export default function todoApp(state = {}, action) {
//   return {
//     visibilityFilter: visibilityFilter(state.visibilityFilter, action),
//     todos: todos(state.todos, action),
//   }
// }
export default todoApp

index.js:

import { createStore } from 'redux'
import todoApp from './reducers'
// createStore 方法创建一个Store,createStore的第二个参数是可选的,用于设置state的初始状态。
let store = createStore(todoApp)

发起 Actions:

import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'

// 获取初始状态
console.log(store.getState())

// subscribe 监听 state 的更新,每次更新会执行回调函数
// subscribe() 会返回一个函数 unsubscribe,用于注销监听器
const unsubscribe = store.subscribe(() => {
  //...
})

// 发起actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(toggleTodo(0))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))

// 注销监听器
unsubscribe()

时间旅行

由于 Redux 应用程序的状态是在一个线性的可预测的时间线上生成的,在项目开发过程中,可以利用时间旅行模式更方便地调试。在每一个 action 触发后,将老的状态树保存下来,调试的时候可以旅行到任意版本的 state,就实现了时间旅行功能。

Redux 的特点

  • 单向数据流,Redux 继承了 Flux 单向数据流的特性,View 发出 Action store.dispatch(action),Store 调用 Reducer 计算出新的 state,如果 state 产生变化,则调用监听函数重新渲染 View store.subscribe(render)

  • 单一数据源,Redux 不同于 Flux,只有一个 Store,里面包含应用程序所有 state。

  • 静态 Store,不应该直接改变原有的 State,而是在原有的基础上生成新的 State。

  • 没有 Dispatcher,而是作为 Store 的一个接口 store.dispatch(action)

Vuex

Vue 自己有一套状态管理,就是 Vuex,和 Flux,Redux 的思想很类似。

6.png

Vuex 也是单向数据流,用户操作 View 或者其他情况触发 Action,Action 提交 Mutation 改变 state。

  • Store: Vuex 和 Redux 一样,整个应用程序中只有一个 Store,管理着全部状态。

  • Mutation: 提交 Mutation 是唯一可以改变 state 的方式,每个 Mutation 都有一个 type(类型)和 handler(回调函数),这和 Redux 的 Reducer 非常相似,但又有所不同,Vuex 不需要每次改变 state 都返回一个新的 state,可以直接改。

  • Action: 在 Redux 中,处理异步修改 state 的时候需要使用中间件,Vuex 给出了 Action 这个解决方案,Action 的工作方式很想 Mutation 和 Redux Action,但是 Action 不会直接改变 state,而是在进行完异步请求后调用 Mutation 更新 state。

为什么要将同步操作放在 Mutation,异步操作放在 Action 中呢?因为这有助于使用 devtools 追踪状态的变化,更好地调试。

1607258686_1_.png

Vuex 和 Redux 的不同

  • Vuex 十分贴合 Vue 的响应式机制,只适合 Vue,Redux 则是一个泛用的实现。

  • Vuex 的 Store 并不是不可变的,虽然 Redux Store 的不变性(Immutability)可以让每一次 state 的变化变得可追踪,但是带来的性能影响也不小。

  • Vuex API 设计简单,Redux 为了实现同构(同一份代码可以在不同环境下运行, 功能呢组件能够在客户端渲染, 也能够在服务端生成 HTML)将 API 设计的较为繁琐。

  • 改变 state 的流程不同

    Redux 操作状态(同步异步相同):View ➡ Actions ➡ Reducer ➡ 改变 state ➡ View 变化

    Vuex 操作状态:

    • 同步:View ➡ Commit ➡ Mutations ➡ 改变 state ➡ View 变化

    • 异步:View ➡ Dispatch ➡ Actions ➡ 改变 state ➡ View 变化

MobX

MobX 原名 Mobservable,而后改名为 MobX,MobX 吸收了 Vue,Knockout 等 MVVM 框架的思想,可以在任何符合 ES5 的 JavaScript 环境下使用。

MobX 和 Redux 的函数式编程不同,是基于观察者模式和面向对象的状态管理,它的思想很简单:状态只要一变,其他用到状态的地方就都跟着自动变

使用 MobX 将一个应用变成响应式分为三个步骤:

  • 定义状态并使其可观察,MobX 允许你将任何数据结构作为状态存储的载体,如数组,对象,类等等都没关系,只要将属性传给 observable 就好。

  • 创建视图以响应状态的变化,MobX 会以最小限度的方式来更新视图(只有用到的数据才会引发绑定,局部精确更新),任何函数都可以成为可以观察自身数据的响应式视图。

  • 更改状态,MobX 不会命令你如何如何去做。这是最佳实践,但关键要记住一点: MobX 帮助你以一种简单直观的方式来完成工作。

1607305228_1_.png

MobX 的核心概念

  • State:驱动应用的数据。

  • Derivations(衍生):任何源自状态并且不会再有任何进一步的相互作用的东西就是衍生,MboX 将衍生分为两种:

    • Computed values:计算值指的是永远可以使用纯函数从当前可观察状态中衍生出的值,这和 VueX 的 getter 没有太大区别。

    • Reactions:Reactions 是当状态改变时需要发生的副作用。

    如果你想创建一个基于当前状态的值时,请使用 computed。

  • Actions:改变 State 的动作,相比 Redux,MobX 把 Reducer 做的事情全塞到 Action 中了,由 Action 改变 State。

从上图我们可以看出来,MobX 同样支持单向数据流,但 MobX 中对状态的修改在时间上都是无法回溯的,因为 MobX Store 是 mutable(可变) 的。

const obj = observable({
  a: 1,
  b: 2,
})

autoRun(() => {
  console.log(obj.a)
})

obj.b = 3 // 什么都没有发生
obj.a = 2 // observe 函数的回调触发了,控制台输出:2

以上代码可以充分的展现 MobX 对数据粒度精细的控制,没有用到的数据,即使是添加到了 observable 中,也不会引发绑定。

MobX 的特点

  • Mutable Store,同 VueX 一样,MobX 的 Store 是可变的,因此没有时间回溯功能,可以使用 Mobx State Tree 插件。

  • 往往是多个 Store

  • 局部精确更新,用到的数据才会引发绑定。

  • 基于观察者模式,面向对象的状态管理工具。

  • 没有中间件和时间回溯,在大项目中使用有困难。

  • 双向绑定数据流,MobX 是双向数据流,但也支持单向数据流,为了避免双向绑定造成的 state 级联更新,建议还是遵循单向数据流模式。

Dva

Dva 是基于 Redux 和 Redux-Saga 的一个状态管理解决方案,内置了 react-router 和 fetch,所以也可以理解为是一个轻量级的应用框架。

PPrerEAKbIoDZYr.png

从上图可以知道,用户操作视图或其他外部行为会通过 dispatch 触发一个 Action,如果是同步行为会通过 Reducer 改变 State,如果是异步行为则通过 Effect 改变 State,Dva 和 VueX 同样将同步和异步操作区分开来使得数据流向非常清晰简明。

  • State:Model 中的状态数据,可以是任何数据类型,和 Redux 一样,Dva 也遵循 immutability(不变性)原则,保证每次返回的否是全新的 state 对象。

  • Action:Action 是一个普通的 Javascript 对象,是改变 state 的唯一途径,每个 Action 都含有 type 属性作为类型。

  • dispatch:是一个用于触发 Action 的函数,和 Reducer 不同,Action 是行为,而 dispatch 只是触发 Action 的方式,Reducer 描述的是数据是如何改变的。

  • Reducer:在 Dva 中,Reducer 必须是纯函数, 通过 Action 中传入的值,与当前 Reducer 中的值进行运算获取新的 State。

  • Effect:Effect 被称为副作用,用来处理异步行为下的 State 改变,这里引入了 Redux-Saga 做异步流程控制,采用了 generator 函数来化异步为同步写法。

  • Subscription:是一种从源获取数据的方法,用于订阅数据源,然后根据条件 dispatch 需要的 Action。

Dva 特点

  • 基于 Redux 和 Redux-Saga。

  • API 少,比较简洁好用。

参考文章