两大状态管理框架的对比
引言
随着 JavaScript 单页应用开发日趋复杂,JavaScript 需要管理比任何时候都要多的 state (状态)。 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。
构建大型SPA时,复杂的状态通常会使开发者比较头疼,而Flux就是一种前端状态管理架构思想,专门解决软件的结构问题。贯穿Flux的一个核心概念是单向数据流(undirectional data flow)。
基于Flux的设计思想,出现了一批前端状态管理框架。他们给出了一些库用于实现Flux的思想,并在Flux的基础上做了一些改进。
在这些框架里,当前最热门的莫过于Redux
和Vuex
了。
Vuex
概念
先看一下官网里vuex的数据流向图:
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
从图中就可以看出Vuex由几个核心概念组成:
- State
Vuex 使用单一状态树——是的,用一个对象就包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在。这也意味着,每个应用将仅仅包含一个 store 实例。单一状态树让我们能够直接地定位任一特定的状态片段,在调试的过程中也能轻易地取得整个当前应用状态的快照。
组件仍然保有局部状态
使用 Vuex 并不意味着你需要将所有的状态放入 Vuex。虽然将所有的状态放到 Vuex 会使状态变化更显式和易调试,但也会使代码变得冗长和不直观。如果有些状态严格属于单个组件,最好还是作为组件的局部状态。你应该根据你的应用开发需要进行权衡和确定。 - Getter
有时候我们需要从 store 中的 state 中派生出一些状态,Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性)。就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。 - Mutations
更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)
和 一个回调函数 (handler)
。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数
Mutation 必须是同步函数 - Action
Action 类似于 mutation,不同在于:
Action 提交的是 mutation,而不是直接变更状态。
Action 可以包含任意异步操作。 - Module
由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store 对象就有可能变得相当臃肿。为了解决以上问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割
一个简单的例子
state
1 | // initial state |
getters
1 | const getters = { |
actions
1 | const actions = { |
mutations
1 | const mutations = { |
总得来说,vuex的数据流还是很容易理解的。对于大型应用,就可以使用module来分割各模块,各模块拥有自己的state\getters\actions\mutations。我们会希望把 Vuex 相关代码分割到模块中。下面是项目结构示例:
1 | ├── index.html |
Redux
概念
相比来说,redux的数据流就要复杂一些了。View调用store.dispatch发起Action -> store接受Action(action经过中间件处理传入reducer函数,reducer函数返回一个新的state)->通知store.subscribe订阅的重新渲染函数
Redux 可以用这三个基本原则来描述:
- 单一数据源
- State只读
- 使用纯函数来执行修改
也有几个相似的基本概念:
- Action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过store.dispatch()
将 action 传到 store。 - Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。 - Store
在前面的章节中,我们学会了使用 action 来描述“发生了什么”,和使用 reducers 来根据 action 更新 state 的用法。Store 就是把它们联系到一起的对象。Store 有以下职责:
维持应用的 state;
提供getState()
方法获取 state;
提供dispatch(action)
方法更新 state;
通过subscribe(listener)
注册监听器;
通过subscribe(listener)
返回的函数注销监听器。
这几个概念都比较容易理解,个人觉得比较难的点是如何将它们组合起来,如拆分reducers,使用 React Redux 库的 connect()
方法实现容器组件从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。
一个简单的例子
合并reducers
1 | import * as reducers from './reducers'; |
在reducers/index.js中
1 | import { hospitalReducer, userReducer } from './hospital'; |
在reducers/hospital.js中
1 | const hospitalReducerInitialState = ((hospital) => { |
然后在组件中定义mapStateToProps就可以读取state中的数据了
1 | const mapStateToProps = (state: any) => { |
当然还有真正接受action的reducer:
1 | export function userCollectReducer(state = initialState, action) { |
接下来讲的Action就更复杂了。先考虑同步Action的情况:
1 | export const setItem = (options) => { |
但是异步操作怎么办?Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。
redux-thunk中间件可以让action创建函数先不返回一个action对象,而是返回一个函数,函数传递两个参数(dispatch,getState),在函数体内进行业务逻辑的封装,而且可以再次dispatch一个action
1 | const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; |
redux-action-tools是另一种比较好的方案。XX_SUCCESS/XX_FAILED相关的代码都被封装了起来,不用为每个异步请求写3个Action
1 | const GET_DATA = 'GET_DATA'; |
总结
Vuex数据流的顺序是:
View调用store.commit(通过actions)提交对应的请求到Store中对应的mutation函数->store改变(vue检测到数据变化自动渲染)
而Redux数据流的顺序是:
View调用store.dispatch发起Action->store接受Action(action传入reducer函数,reducer函数返回一个新的state)->通知store.subscribe订阅的重新渲染函数
Vuex是专门为Vue设计的状态管理框架,同样基于Flux架构,并吸收了Redux的优点
Vuex相对于Redux的不同点有:
- 改进了Redux中的Action和Reducer函数,以mutations变化函数取代Reducer,
- 无需switch,只需在对应的mutation函数里改变state值即可
由于Vue自动重新渲染的特性,无需订阅重新渲染函数,只要生成新的State即可。store和state是最基本的概念,VUEX没有做出改变。其实VUEX对整个框架思想并没有任何改变,只是某些内容变化了名称或者叫法,通过改名,以图在一些细节概念上有所区分。
- VUEX弱化了dispatch的存在感。VUEX认为状态变更的触发是一次“提交”而已,而调用方式则是框架提供一个提交的commit API接口。
- VUEX也弱化了Redux中的reducer的概念。reducer在计算机领域语义应该是”规约”,在这里意思应该是根据旧的state和Action的传入参数,”规约”出新的state。在VUEX中,对应的是mutation,即”转变”,只是根据入参对旧state进行”转变”而已。
总的来说,VUEX通过弱化概念,在任何东西都没做实质性削减的基础上,使得整套框架更易于理解了。
另外VUEX支持getter,运行中是带缓存的,算是对提升性能方面做了些优化工作,言外之意也是鼓励大家多使用getter。
vuex已经整合好了状态管理的需要的大部分功能,例如异步,所以写着比较简单。redux是未简化版本的状态管理,保留核心的状态管理思想,可以自定义状态的多种处理方式,配合其他第三方库来一起使用。