单向数据流-从共享状态管理:flux/redux/vuex漫谈异步数据处理
Author:zhoulujun Date:
不管是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的最大特点就是数据都是单向流动的。
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 里面只有一个 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 的整合
Redux则是一个纯粹的状态管理系统,react-redux是常规的状态管理系统(Redux)与React框架的结合版本——React利用React-Redux将它与React框架结合起来。React-Redux还有一些衍生项目,DVA就是一个基于对React-Redux进行封装并提供了一些优化特性的框架。
Redux原理
Redux 的实现原理非常简单,不考虑中间件的情况下,甚至可以说短短几十行就够了。核心源码都在 createStore 和 combineReducers 里面。
在 createStore 里面,最终会返回一个 store,它主要拥有 getState、dispatch、subscribe、unsubscribe 等几个方法。
这里是简化了 Redux 源码后 createStore 的一个简单实现,它的核心就是一个 发布-订阅 模式。
const createStore = (reducer, initialState, enhancer) => { // 如果传入了 applyMiddleware,那就调用它 if (enhancer && typeof enhancer === 'function') return enhancer(createStore)(reducer,initialState) let state = initialState,listeners = [],isDispatch = false; // 获取 store const getState = () => state; // 发送一个 action const dispatch = (action) => { // action 不能同时发送 if (isDispatch) return action; isDispatch = true; state = reducer(state, action); isDispatch = false; // 执行注册的事件 listeners.forEach(listener => listener(state)); return action; } // 监听 store 变化,注册事件 const subscribe = (listener) => { if (typeof listener === "function") { listeners.push(listener); } return () => unsubscribe(listener); } // 移除注册的事件 const unsubscribe = (listener) => { const index = listeners.indexOf(listener); listeners.splice(index, 1); } return { getState,dispatch,subscribe,unsubscribe } }
看到 subscribe 就会明白 React-redux 是怎么做的 bind。connect 本身也是一个高阶组件,我们通过 Provider 将 store 传给子孙组件。在 connect 里面通过 subscribe 监听了 store,一旦 store 变化,它就让 React 组件重新渲染。
const connect = (mapStateToProps, mapDispathToProps) => (WrappedComponent) => { return class extends React.Component { static contextType = ReactReduxContext; constructor(props) { super(props); this.store = this.context.store; this.state = { state: this.store.getState() }; } componentDidMount() { this.store.subscribe((nextState) => { // 浅比较 if (!shadowCompare(nextState, this.state.state)) { this.setState({ state: nextState }); } }); } render() { const props = { ...mapStateToProps(this.state.state), ...mapDispathToProps(this.state.state), ...this.props } return <WrappedComponent {...props} /> } } }
而另一部分的 combineReducers,则是在每次更新的时候去遍历执行最初传入的 reducer。
const combineReducers = reducers => { const finalReducers = {}, nativeKeys = Object.keys; nativeKeys(reducers).forEach(reducerKey => { // 过滤掉不是函数的 reducer if(typeof reducers[reducerKey] === "function") { finalReducers[reducerKey] = reducers[reducerKey]; } }) // 返回了一个新的函数 return (state, action) => { let hasChanged = false; let nextState = {}; // 遍历所有的 reducer 函数并执行 nativeKeys(finalReducers).forEach(key => { const reducer = finalReducers[key]; nextState[key] = reducer(state[key], action); hasChanged = hasChanged || nextState[key] !== state[key] }) return hasChanged ? nextState : state; } }
从上面的源码也可以看出来,Redux 存在一个很明显的问题,那就是需要通过遍历 reducer 来匹配到对应的 action.type。
那么这里有没有优化空间呢?为什么 action 和 reducer 必须手写 switch...case 来匹配呢?如果将 action.type 作为函数名,这样是否就能减少心智负担呢?
这些很多人都想到了,所以 Rematch 和 Dva 就在这之上做了一系列优化,Redux 也吸取了他们的经验,重新造了 @reduxjs/toolkit。
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。其它的都回滚。
副作用(Side Effect)
side effect出自于“函数式编程”,这种编程范式鼓励我们多使用“纯函数”。显然,大多数的异步任务都需要和外部世界进行交互,不管是发起网络请求、访问本地文件或是数据库等等,因此,它们都会产生“副作用”。
纯函数特性
输出不受外部环境影响:同样的输入一定可以获得同样的输出
不影响外部环境:包括但不限于修改外部数据、发起网络请求、触发事件等等
为什么要多用纯函数呢?因为它们具有很强的“可预测性”。既然有纯函数,那肯定有不纯的函数喽,或者换个说法,叫做有“副作用”的函数。
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的优点。
###### 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
各流派 React 状态管理对比和原理实现 https://github.com/yinguangyao/blog/issues/56
转载本站文章《单向数据流-从共享状态管理:flux/redux/vuex漫谈异步数据处理》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8440.html