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

栏目: 服务器 · 发布时间: 5年前

内容简介:相信大家在项目开发中,在页面较复杂的情况下,往往会遇到一个问题,就是在页面组件之间通信会非常困难。比如说一个商品列表和一个已添加商品列表:假如这两个列表是独立的两个组件,它们会共享一个数据 “

相信大家在项目开发中,在页面较复杂的情况下,往往会遇到一个问题,就是在页面组件之间通信会非常困难。

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

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

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

它们两个是兄弟组件,在没有数据流框架的帮助下,在组件内数据有变化的时候,只能通过父组件传输数据,往往会有 onSelectedDataChange 这种函数出现,在这种情况下,还尚且能忍受,如果组件嵌套较深的话,那痛苦可以想象一下,所以才有解决数据流的各种框架的出现。

本质分析

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

V = f(M)
复制代码

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

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

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

我们知道数据是不会自己改变,那么肯定是有“外力”去推动,往往是远程请求数据回来或者是 UI 上的交互行为,我们统称这些行为叫 action

ΔM = perform(action) 
复制代码

每一个 action 都会去改变数据,那么视图得到的 数据(state) 就是所有 action 叠加起来的变更,

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

所以真实的场景会出现如下或更复杂的情况:

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

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

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

数据流框架

Redux

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

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

它的原理比较简单, 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 去托管,甚至有人所有状态都有组件托管,也是存在的,因为页面太简单,根本就不需要用到数据流框架。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Powerful

Powerful

Patty McCord / Missionday / 2018-1-25

Named by The Washington Post as one of the 11 Leadership Books to Read in 2018 When it comes to recruiting, motivating, and creating great teams, Patty McCord says most companies have it all wrong. Mc......一起来看看 《Powerful》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具