在 React 中处理数据流问题的一些思考

背景

相信你们在项目开发中,在页面较复杂的状况下,每每会遇到一个问题,就是在页面组件之间通讯会很是困难。ios

好比说一个商品列表和一个已添加商品列表:git

假如这两个列表是独立的两个组件,它们会共享一个数据 “被选中的商品”,在商品列表选中一个商品,会影响已添加商品列表,在已添加列表中删除一个商品,一样会影响商品列表的选中状态。github

它们两个是兄弟组件,在没有数据流框架的帮助下,在组件内数据有变化的时候,只能经过父组件传输数据,每每会有 onSelectedDataChange 这种函数出现,在这种状况下,还尚且能忍受,若是组件嵌套较深的话,那痛苦能够想象一下,因此才有解决数据流的各类框架的出现。redux

本质分析

咱们知道 React 是 MVC 里的 V,而且是数据驱动视图的,简单来讲,就是数据 => 视图,视图是基于数据的渲染结果:axios

V = f(M)
复制代码

数据有更新的时候,在进入渲染以前,会先生成 Virtual DOM,先后进行对比,有变化才进行真正的渲染。api

V + ΔV = f(M + ΔM)
复制代码

数据驱动视图变化有两种方式,一种是 setState,改变页面的 state,一种是触发 props 的变化。bash

咱们知道数据是不会本身改变,那么确定是有“外力”去推进,每每是远程请求数据回来或者是 UI 上的交互行为,咱们统称这些行为叫 actionmarkdown

ΔM = perform(action) 
复制代码

每个 action 都会去改变数据,那么视图获得的数据(state)就是全部 action 叠加起来的变动,app

state = actions.reduce(reducer, initState)
复制代码

因此真实的场景会出现以下或更复杂的状况:框架

问题就出在,更新数据比较麻烦,混乱,每次要更新数据,都要一层层传递,在页面交互复杂的状况下,没法对数据进行管控。

有没有一种方式,有个集中的地方去管理数据,集中处理数据的接收修改分发?答案显然是有的,数据流框架就是作这个事情,熟悉 Redux 的话,就知道其实上面讲的就是 Redux 的核心理念,它和 React 的数据驱动原理是相匹配的。

数据流框架

Redux

数据流框架目前占主要地位的仍是 Redux,它提供一个全局 Store 处理应用数据的接收修改分发

它的原理比较简单,View 里面有任何交互行为须要改变数据,首先要发一个 action,这个 actionStore 接收并交给对应的 reducer 处理,处理完后把更新后的数据传递给 ViewRedux 不依赖于任何框架,它只是定义一种方式控制数据的流转,能够应用于任何场景。

虽然定义了一套数据流转的方式,但真正使用上会有很多问题,我我的总结主要是两个问题:

  1. 定义过于繁琐,文件多,容易形成思惟跳跃。
  2. 异步流的处理没有优雅的方案。

咱们来看看写一个数据请求的例子,这是很是典型的案例:

actions.js

export const FETCH_DATA_START = 'FETCH_DATA_START';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR';

export function fetchData() {
  return dispatch => {
    dispatch(fetchDataStart());
    axios.get('xxx').then((data) => {
      dispatch(fetchDataSuccess(data));
    }).catch((error) => {
      dispatch(fetchDataError(error));
    });
  };
}

export function fetchDataStart() {
  return {
    type: FETCH_DATA_START,
  }
}

...FETCH_DATA_SUCCESS
...FETCH_DATA_ERROR

复制代码

reducer.js

import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR  } from 'actions.js';
export default (state = { data: null }, action) => {
  switch (action.type) {
    case FETCH_DATA_START:
      ...
    case FETCH_DATA_SUCCESS:
      ...
    case FETCH_DATA_ERROR:
      ...
    default: 
      return state
  }
}
复制代码

view.js

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from 'reducer.js';
import { fetchData } from 'actions.js';

const store = createStore(reducer, applyMiddleware(thunk));
store.dispatch(fetchData());
复制代码

第一个问题,发一个请求,由于须要托管请求的全部状态,因此须要定义不少的 action,这时很容易会绕晕,就算有人尝试把这些状态再封装抽象,也会充斥着一堆模板代码。有人会挑战说,虽然一开始是比较麻烦,繁琐,但对项目可维护性,扩展性都比较友好,我不太认同这样的说法,目前还算简单,真正业务逻辑复杂的状况下,会显得更恶心,效率低且阅读体验差,相信你们也写过或看过这样的代码,后面本身看回来,须要在 actions 文件搜索一下 action 的名称,reducer 文件查询一下,绕一圈才慢慢看懂。

第二个问题,按照官方推荐使用 redux-thunk 实现异步 action 的方法,只要在 action 里返回一个函数便可,这对有强迫症的人来讲,简直受不了,actions 文件显得它很不纯,原本它只是来定义 action,却居然要夹杂着数据请求,甚至 UI 上的交互!

我以为 Redux 设计上没有问题,思路很是简洁,是我很是喜欢的一个库,它提供的数据的流动方式,目前也是获得社区的普遍承认。然而在使用上有它的缺陷,虽然是能够克服,可是它自己难道没有能够优化的地方?

dva

dva 的出来就是为了解决 redux 的开发体验问题,它首次提出了 model 的概念,很好地把 actionreducersstate 结合到一个 model 里面。

model.js

export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
};
复制代码

它的核心思想就是一个 action 对应一个 reducer,经过约定,省略了对 action 的定义,默认 reducers 里面的函数名称即为 action 的名称。

在异步 action 的处理上,定义了 effects(反作用) 的概念,与同步 action 区分起来,内部借助了 redux-saga 来实现。

model.js

export default {
  namespace: 'counter',
  state: [],
  reducers: {
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
};
复制代码

经过这样子的封装,基本保持 Redux 的用法,咱们能够沉浸式地在 model 编写咱们的数据逻辑,我以为已经很好地解决问题了。

不过我我的喜爱问题,不太喜欢使用 redux-saga 这个库来解决异步流,虽然它的设计很巧妙,利用了 generator 的特性,不侵入 action,而是经过中间件的方式进行拦截,很好地将异步处理隔离出独立的一层,而且以此声称对实现单元测试是最友好的。是的,我以为设计上真的很是棒,那时候还特地阅读了它的源码,赞叹做者真的牛,这样的方案都能想出来,可是后来我看到还有更好的解决方案(后面会介绍),就放弃使用它了。

mirrorx

mirrorxdva 差很少,只是它使用了单例的方式,全部的 action 都保存了 actions 对象中,访问 action 有了另外一种方式。还有就是处理异步 action 的时候可使用 async/await 的方式。

import mirror, { actions } from 'mirrorx'

mirror.model({
  name: 'app',
  initialState: 0,
  reducers: {
    increment(state) { return state + 1 },
    decrement(state) { return state - 1 }
  },
  effects: {
    async incrementAsync() {
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve()
        }, 1000)
      })
      actions.app.increment()
    }
  }
});
复制代码

它内部处理异步流的问题,相似 redux-thunk 的处理方式,经过注入一个中间件,这个中间件里判断 当前 action 是否是异步 action(只要判断是否是 effects 里定义的 action 便可),若是是的话,就直接中断了中间件的链式调用,能够看看这段代码

这样的话,咱们 effects 里的函数就可使用 async/await 的方式调用异步请求了,其实不是必定要使用 async/await,函数里的实现没有限制,由于中间件只是调用函数执行而已。

我是比较喜欢使用 async/await 这种方式处理异步流,这是我不用 redux-saga 的缘由。

xredux

可是我最终没有选择使用 mirrorxdva,由于用它们就捆绑一堆东西,我以为不该该作成这样子,为啥好好的解决 Redux 问题,最后变成都作一个脚手架出来?这不是强制消费吗?让人用起来就会有限制。了解它们的原理后,我本身参照写了个 xredux 出来,只是单纯解决 Reudx 的问题,不依赖于任何框架,能够看做只是 Redux 的升级版。

使用上和 mirrorx 差很少,但它和 Redux 是同样的,不绑定任何框架,能够独立使用。

import xredux from "xredux";

const store = xredux.createStore();
const actions = xredux.actions;

// This is a model, a pure object with namespace, initialState, reducers, effects.
xredux.model({
  namespace: "counter",
  initialState: 0,
  reducers: {
    add(state, action) { return state + 1; },
    plus(state, action) { return state - 1; },
  },
  effects: {
    async addAsync(action, dispatch, getState) {
      await new Promise(resolve => {
        setTimeout(() => {
          resolve();
        }, 1000);
      });
      actions.counter.add();
    }
  }
});

// Dispatch action with xredux.actions
actions.counter.add();
复制代码

在异步处理上,其实也存在问题,可能你们也遇到过,就是数据请求有三种状态的问题,咱们来看看,写一个数据请求的 effects

import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';

const { actions } = xredux;

xredux.model({
  namespace: 'user',
  initialState: {
    getUserInfoStart: false,
    getUserInfoError: null,
    userInfo: null,
  },
  reducers: {
    // fetch start
    getUserInfoStart (state, action) {
      return {
        ...state,
        getUserInfoStart: true,
      };
    },
    // fetch error
    getUserInfoError (state, action) {
      return {
        ...state,
        getUserInfoStart: false,
        getUserInfoError: action.payload,
      };
    },
    // fetch success
    setUserInfo (state, action) {
      return {
        ...state,
        userInfo: action.payload,
        getUserInfoStart: false,
      };
    }
  },
  effects: {
    async getUserInfo (action, dispatch, getState) {
      let userInfo = null;
      actions.user.getUserInfoStart();
      try {
        userInfo = await fetchUserInfo();
        actions.user.setUserInfo(userInfo);
      } catch (e) {
        actions.user.setUserInfoError(e);
      }
    }
  },
});
复制代码

能够看到,仍是存在不少感受没用的代码,一个请求须要3个 reducer 和1个 effect,当时想着怎么优化,但没有很好的办法,后来我想到这3个 reducer 有个共同点,就是只是赋值,没有任何操做,那我内置一个 setStatereducer,专门去处理这种只是赋值的 action 就行了。

最后变成这样:

import xredux from 'xredux';
import { fetchUserInfo } from 'services/api';

const { actions } = xredux;

xredux.model({
  namespace: 'user',
  initialState: {
    getUserInfoStart: false,
    getUserInfoError: null,
    userInfo: null,
  },
  reducers: {
  },
  effects: {
    async getUserInfo (action, dispatch, getState) {
      let userInfo = null;
      // fetch start
      actions.user.setState({
        getUserInfoStart: true,
      });
      try {
        userInfo = await fetchUserInfo();
        // fetch success
        actions.user.setState({
          getUserInfoStart: false,
          userInfo,
        });
      } catch (e) {
        // fetch error
        actions.user.setState({
          getUserInfoError: e,
        });
      }
    }
  },
});
复制代码

这个目前是本身比较满意的方案,在项目中也有实践过,写起来确实比较简洁易懂,不知你们有没有更好的办法。

贫血组件/充血组件

使用了 Redux,按道理应用中的状态数据应该都放到 Store 中,那组件是否能有本身的状态呢?目前就会有两种见解:

  • 全部状态都应该在 Store 中托管,全部组件都是纯展现组件。
  • 组件可拥有本身的部分状态,另一些由 Store 托管。

这两种就是分别对应贫血组件和充血组件,区别就是组件是否有本身的逻辑,仍是说只是纯展现。我以为这个问题不用去争论,没有对错。

理论上固然是说贫血组件好,由于这样保证数据是在一个地方管理的,可是付出的代价多是沉重的,使用了这种方式,每每到后面会有想死的感受,一种想回头又不想放弃的感受,其实不必这么执着。

相信你们几乎都是充血组件,有一些状态只与组件相关的,由组件去托管,有些状态须要共享的,交给 Store 去托管,甚至有人全部状态都有组件托管,也是存在的,由于页面太简单,根本就不须要用到数据流框架。

总结

React 开发中不可避免会遇到数据流的问题,如何优雅地处理目前也没有最完美的方案,社区也存在各类各样的方法,能够多思考为何是这样作,了解底层原理比盲目使用别人的方案更重要。

若是想详细了解 xredux 如何在 React 中运用,可使用 RIS 初始化一个 Standard 应用看看,以前的文章《RIS,建立 React 应用的新选择》 有简单提过,欢迎你们体验。

参考资料

相关文章
相关标签/搜索