写点什么

单向数据流 - 从共享状态管理:flux/redux/vuex 漫谈异步数据处理

用户头像
zhoulujun
关注
发布于: 13 小时前

不管是 Vue,还是 React,都需要管理状态(state),比如组件之间都有共享状态的需要。

什么是共享状态?

比如一个组件需要使用另一个组件的状态,或者一个组件需要改变另一个组件的状态,都是共享状态。

父子组件之间,兄弟组件之间共享状态,往往需要写很多没有必要的代码,比如把状态提升到父组件里,或者给兄弟组件写一个父组件,听听就觉得挺啰嗦。

如果不对状态进行有效的管理,状态在什么时候,由于什么原因,如何变化就会不受控制,就很难跟踪和测试了。如果没有经历过这方面的困扰,可以简单理解为会搞得很乱就对了。

对于状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测

Store 模式

最简单的处理就是把状态存到一个外部变量里面,比如:this.$root.$data,当然也可以是一个全局变量。但是这样有一个问题,就是数据改变后,不会留下变更过的记录,这样不利于调试。

所以我们稍微搞得复杂一点,用一个简单的 Store 模式

var store = {  // 存数据  state: {    message: 'Hello!'  },  // action 来控制 state 的改变——不直接去对 state 做改变,而是通过 action 来改变  setMessageAction (newValue) {    // 因为都走action,就可以知道到底改变(mutation)是如何被触发的,出现错误,也可以记录记录日志啥的    this.state.message = newValue  },  clearMessageAction () {    this.state.message = ''  }}
复制代码

组件不允许直接修改属于 store 实例的 state,组件必须通过 action 来改变 state

也就是说,组件里面应该执行 action 来分发 (dispatch) 事件通知 store 去改变。这样约定的好处是:能够记录所有 store 中发生的 state 改变,同时实现能做到记录变更 (mutation)、保存状态快照、历史回滚/时光旅行的先进的调试工具。

Flux

Flux 其实是一种思想,就像 MVC,MVVM 之类的,他给出了一些基本概念,所有的框架都可以根据他的思想来做一些实现。


flux数据流


Flux 的最大特点就是数据都是单向流动的。

  • Dispatcher 的作用是接收所有的 Action,然后发给所有的 Store。这里的 Action 可能是 View 触发的,也有可能是其他地方触发的,比如测试用例。转发的话也不是转发给某个 Store,而是所有 Store。

  • Store 的改变只能通过 Action,不能通过其他方式。也就是说 Store 不应该有公开的 Setter,所有 Setter 都应该是私有的,只能有公开的 Getter。具体 Action 的处理逻辑一般放在 Store 里。

Flux 有一些缺点(特点),比如一个应用可以拥有多个 Store,多个 Store 之间可能有依赖关系;Store 封装了数据还有处理数据的逻辑。

redux

Redux 使用一个对象存储整个应用的状态(global state),当 global state 发生变化时,状态从树形结构的最顶端往下传递。每一级都会去进行状态比较,从而达到更新。

action 可以理解为应用向 store 传递的数据信息(一般为用户交互信息)

dispatch(action) 是一个同步的过程:执行 reducer 更新 state -> 调用 store 的监听处理函数。

reducer 实际上就是一个函数:(previousState, action) => newState。用来执行根据指定 action 来更新 state 的逻辑。通过 combineReducers(reducers)可以把多个 reducer 合并成一个 root reducer。

reducer 不存储 state, reducer 函数逻辑中不应该直接改变 state 对象, 而是返回新的 state 对象(可以考虑使用 immutable-js)。


redux架构示意图


redux 一些特性

  • Redux 里面只有一个 Store,整个应用的数据都在这个大 Store 里面

    Store 的 State 不能直接修改,每次只能返回一个新的 State

    Redux 整了一个 createStore 函数来生成 Store。

    Store 允许使用  store.subscribe  方法设置监听函数,一旦 State 发生变化,就自动执行这个函数

  • Action 必须有一个 type 属性,代表 Action 的名称,其他可以设置一堆属性,作为参数供 State 变更时参考

    Redux 可以用 Action Creator 批量来生成一些 Action。

  • Reducer 纯函数来处理事件。Store 收到 Action 以后,必须给出一个新的 State(就是刚才说的 Store 的 State 不能直接修改,每次只能返回一个新的 State),这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。

    Redux  里每一个 Reducer 负责维护 State 树里面的一部分数据

    多个 Reducer 可以通过 combineReducers 方法合成一个根 Reducer,这个根 Reducer 负责维护整个 State

  • Redux 没有 Dispatcher 的概念,Store 里面已经集成了 dispatch 方法。store.dispatch()是 View 发出 Action 的唯一方法

redux 与 flux 对比

  • Flux 中 Store 是各自为战的,每个 Store 只对对应的 View 负责,每次更新都只通知对应的 View

  • Redux 中各子 Reducer 都是由根 Reducer 统一管理的,每个子 Reducer 的变化都要经过根 Reducer 的整合


Flux数据管理


redux数据管理


Redux 则是一个纯粹的状态管理系统,react-redux 是常规的状态管理系统(Redux)与 React 框架的结合版本——React 利用 React-Redux 将它与 React 框架结合起来。React-Redux 还有一些衍生项目,DVA 就是一个基于对 React-Redux 进行封装并提供了一些优化特性的框架。

React-redux

Redux 和 Flux 类似,只是一种思想或者规范,它和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

但是因为 React 包含函数式的思想,也是单向数据流,和 Redux 很搭,所以一般都用  Redux 来进行状态管理。为了简单处理  Redux  和 React  UI  的绑定,一般通过一个叫 react-redux 的库和 React 配合使用,这个是  react  官方出的

Redux 将 React 组件分为容器型组件和展示型组件

容器型组件一般通过 connet 函数生成,它订阅了全局状态的变化,通过 mapStateToProps 函数,我们可以对全局状态进行过滤,而展示型组件不直接从 global state 获取数据,其数据来源于父组件。

  • mapStateToProps  把容器组件的 state 映射到 UI 组件的 props

  • mapDispatchToProps 把 UI 组件的事件映射到 dispatch 方法

每一次全局状态发生变化,所有的容器型组件都会得到通知,而各个容器型组件需要通过 shouldComponentUpdate 函数来确实自己关注的局部状态是否发生变化、自身是否需要重新渲染,默认情况下,React 组件的 shouldComponentUpdate 总返回 true,这里貌似有一个严重的性能问题


容器型组件和展示型组件对比


Middleware(中间件)

在  Redux  中

  • 同步的表现就是:Action 发出以后,Reducer 立即算出 State。

  • 异步的表现就是:Action 发出以后,过一段时间再执行 Reducer——在 View 里发送 Action 的时候,加上一些异步操作了。

let next = store.dispatch;// 给原来的  dispatch  方法包裹了一层store.dispatch = function dispatchAndLog(action) {  // TODO   next(action);}
复制代码

单页面应用中充斥着大量的异步请求(ajax)。dispatch(action) 是同步的,如果要处理异步 action,需要使用一些中间件

Redux 提供了一个 applyMiddleware 方法来应用中间件:

const store = createStore(    reducer,    applyMiddleware(thunk, promise, logger));
复制代码

这个方法主要就是把所有的中间件组成一个数组,依次执行。也就是说,任何被发送到 store 的 action 现在都会经过 thunk,promise,logger 这几个中间件了。

处理异步 Action

用  Redux  处理异步,可以自己写中间件来处理,当然大多数人会选择一些现成的支持异步处理的中间件。比如 redux-thunk 或 redux-promise,分别是使用异步回调和 Promise 来解决异步 action 问题的。

  • thunk 就是简单的 action 作为函数,在 action 进行异步操作,发出新的 action。

    缺点就是用户要写的代码有点多,可以看到上面的代码比较啰嗦

  • 而 promise 只是在 action 中的 payload 作为一个 promise,中间件内部进行处理之后,发出新的 action。

    和 redux-thunk 的思想类似,只不过做了一些简化,成功失败手动 dispatch 被封装成自动了:

**封装少,自由度高,但是代码就会变复杂;封装多,代码变简单了,但是自由度就会变差。**redux-thunk 和 redux-promise 刚好就是代表这两个面。

当业务逻辑多且复杂的时候会发生什么情况呢?我们的 action 越来越复杂,payload 越来越长,当然我们可以分离开来单独建立文件去处理逻辑,但是实质上还是对 redux 的 action 和 reducer 进行了污染,让它们变得不纯粹了,action 就应该作为一个信号,而不是处理逻辑,reducer 里面处理要好一些,但是同样会生出几个多余的 action 类型进行处理,而且也只能是 promise,不能做复杂的业务处理。

redux-saga

redux-saga 是一个 Redux 中间件,用来帮你管理程序的副作用。或者更直接一点,主要是用来处理异步 action。

redux-saga 将进行异步处理的逻辑剥离出来,单独执行,利用 generator 实现异步处理。

关于 saga 原理的,推举阅读《前端技术栈(三):redux-saga,化异步为同步

什么是Saga

为了解决分布式系统中的 LLT(Long Lived Transaction-长时运行事务的数据一致性)问题而提出的一个概念。比如网上购物下单后,需要等待付款才最终确定。

LLT 拆成两个子事务,T1 表示“确认订单||预订”事务,T2 表示“发货”事务。如果在规定时间内付款的数据,才执行 T2。其它的都回滚。


saga事件


副作用(Side Effect)

side effect 出自于“函数式编程”,这种编程范式鼓励我们多使用“纯函数”。显然,大多数的异步任务都需要和外部世界进行交互,不管是发起网络请求、访问本地文件或是数据库等等,因此,它们都会产生“副作用”。

纯函数特性
  1. 输出不受外部环境影响:同样的输入一定可以获得同样的输出

  2. 不影响外部环境:包括但不限于修改外部数据、发起网络请求、触发事件等等

为什么要多用纯函数呢?因为它们具有很强的“可预测性”。既然有纯函数,那肯定有不纯的函数喽,或者换个说法,叫做有“副作用”的函数。

redux-saga 的优势

Redux 处理异步的中间件 redux-thunk 和 redux-promise,当然 redux 的异步中间件还有很多,他们可以处理大部分场景,这些中间件的思想基本上都是把异步请求部分放在了  action  creator  中,理解起来比较简单。

redux-saga 采用了另外一种思路,它没有把异步操作放在 action creator 中,也没有去处理 reductor,而是把所有的异步操作看成“线程”,可以通过普通的 action 去触发它,当操作完成时也会触发 action 作为输出。saga 的意思本来就是一连串的事件。

redux-saga 把异步获取数据这类的操作都叫做副作用(Side  Effect),它的目标就是把这些副作用管理好,让他们执行更高效,测试更简单,在处理故障时更容易。

Vuex

Vuex 是专门为 Vue 设计的状态管理框架,同样基于 Flux 架构,并吸收了 Redux 的优点。


VUEX数据流向图


###### Redux

- 核心对象:store

- 数据存储:state

- 状态更新提交接口:==dispatch==

- 状态更新提交参数:带 type 和 payload 的==Action==

- 状态更新计算:==reducer==

- 限制:reducer 必须是纯函数,不支持异步

- 特性:支持中间件


###### VUEX

- 核心对象:store

- 数据存储:state

- 状态更新提交接口:==commit==

- 状态更新提交参数:带 type 和 payload 的 mutation==提交对象/参数==

- 状态更新计算:==mutation handler==

- 限制:mutation handler 必须是非异步方法

- 特性:支持带缓存的 getter,用于获取 state 经过某些计算后的值

Vuex 相对于 Redux 的不同点有:

改进了 Redux 中的 Action 和 Reducer 函数,以 mutations 变化函数取代 Reducer,无需 switch,只需在对应的 mutation 函数里直接改变 state 值即可(无需返回新的 state)

尤大的说法:Redux 强制的 immutability,在保证了每一次状态变化都能追踪的情况下强制的 immutability 带来的收益很有限,为了同构而设计的 API 很繁琐,必须依赖第三方库才能相对高效率地获得状态树的局部状态,这些都是 Redux 不足的地方,所以也被 Vuex 舍掉了。

由于 Vue 自动重新渲染的特性,无需订阅重新渲染函数,只要修改 State 即可

Flux、Redux、Vuex 三个的思想都差不多,在具体细节上有一些差异,总的来说都是让 View 通过某种方式触发 Store 的事件或方法,Store 的事件或方法对 State 进行修改或返回一个新的 State,State 改变之后,View 发生响应式改变

Vuex 数据流的顺序是:

  • View 调用 store.commit 提交对应的请求到 Store 中对应的 mutation 函数->store 改变(vue 检测到数据变化自动渲染)

  • redux 推荐使用 Object.assign() 新建了一个副本,但是 Vue 定义每一个响应式数据的 ob 都是不可枚举的

Vuex 异步 action

mutation 都是同步事务,

对比 Redux 的中间件,Vuex 加入了 Action 这个东西来处理异步,Vuex 的想法是把同步和异步拆分开,异步操作想咋搞咋搞,但是不要干扰了同步操作。View 通过 store.dispatch('increment') 来触发某个 Action,Action 里面不管执行多少异步操作,完事之后都通过 store.commit('increment') 来触发 mutation,一个 Action 里面可以触发多个 mutation。所以 Vuex 的 Action 类似于一个灵活好用的中间件。

区分 actions 和 mutations 并不是为了解决竞态问题,而是为了能用 devtools 追踪状态变化。

事实上在 vuex 里面 actions 只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发 mutation 就行。异步竞态怎么处理那是用户自己的事情。vuex 真正限制你的只有 mutation 必须是同步的这一点(在 redux 里面就好像 reducer 必须同步返回下一个状态一样)。

同步的意义在于这样每一个 mutation 执行完成后都可以对应到一个新的状态(和 reducer 一样),这样 devtools 就可以打个 snapshot 存下来,然后就可以随便 time-travel 了。

如果你开着 devtool 调用一个异步的 action,你可以清楚地看到它所调用的 mutation 是何时被记录下来的,并且可以立刻查看它们对应的状态。其实我有个点子一直没时间做,那就是把记录下来的 mutations 做成类似 rx-marble 那样的时间线图,对于理解应用的异步状态变化很有帮助。

作者:尤雨溪 . 链接:https://www.zhihu.com/question/48759748/answer/112823337

Vuex 把同步和异步操作通过 mutation 和 Action 来分开处理,是一种方式。但不代表是唯一的方式,还有很多方式,比如就不用 Action,而是在应用内部调用异步请求,请求完毕直接 commit mutation,当然也可以。

Module

Vuex 引入了 Module 的概念,每个 Module 有自己的 state、mutation、action、getter,其实就是把一个大的 Store 拆开。

React-Redux vs VUEX 对比分析

和组件结合方式的差异

通过 VUEX 全局插件的使用,结合将 store 传入根实例的过程,就可以使得 store 对象在运行时存在于任何 vue 组件中

而 React-Redux 则除了需要在较外层组件结构中使用<Provider/>以拿到 store 之外,还需要显式指定容器组件,即用 connect 包装一下该组件。这样看来我认为 VUE 是更推荐在使用了 VUEX 的框架中的每个组件内部都使用 store,而 React-Redux 则提供了自由选择性。而 VUEX 即不需要使用外层组件,也不需要类似 connect 方式将组件做一次包装,我认为出发点应该是可能是为了避免啰嗦。

容器组件的差异

React-Redux 提倡容器组件和表现组件分离的最佳实践,而 VUEX 框架下不做区分,全都是表现(展示)组件。我觉得不分优劣,React-Redux 的做法更清晰、更具有强制性和规范性,而 VUEX 的方式更加简化和易于理解。


总的来说,就是谁包谁,谁插谁的问题。Redux 毕竟是独立于 React 的状态管理,它与 React 的结合则需要对 React 组件进行一下外包装。而 VUEX 就是为 VUE 定制,作为插件、以及使用插入的方式就可以生效,而且提供了很大的灵活性。


参考文章

Vuex、Flux、Redux、Redux-saga、Dva、MobX https://juejin.im/post/5c18de8ef265da616413f332

react-redux 与 Vue-vuex 的原理比较 https://www.yaruyi.com/article/redux-vuex

Vuex 与 Redux 对比 https://blog.csdn.net/hyupeng1006/article/details/80755667


转载本站文章《单向数据流-从共享状态管理:flux/redux/vuex漫谈异步数据处理》,请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8440.html

发布于: 13 小时前阅读数: 3
用户头像

zhoulujun

关注

还未添加个人签名 2021.06.25 加入

还未添加个人简介

评论

发布
暂无评论
单向数据流-从共享状态管理:flux/redux/vuex漫谈异步数据处理