• home > webfront > ECMAS > vue >

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

    Author:zhoulujun Date:

    状态管理的解决思路就是:把组件之间需要共享的状态抽取出来,遵循特定的约定,统一来管理,让状态的变化可以预测。是Vue,还是React,都需要管理状态state。react采用redux集中方案,vue直接给出vuex一把梭。需要注意异步操作

    不管是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进行封装并提供了一些优化特性的框架。

    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。其它的都回滚。

    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

    各流派 React 状态管理对比和原理实现 https://github.com/yinguangyao/blog/issues/56




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