专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
前端早读课  ·  【第3453期】圈复杂度在转转前端质量体系中的应用 ·  22 小时前  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
奇舞精选  ·  从 DeepSeek 看25年前端的一个小趋势 ·  昨天  
前端早读课  ·  【第3452期】React 开发中使用开闭原则 ·  昨天  
前端早读课  ·  【第3451期】前端 TypeError ... ·  2 天前  
51好读  ›  专栏  ›  前端外刊评论

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

前端外刊评论  · 公众号  · 前端  · 2019-03-07 07:00

正文

背景

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

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

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

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

本质分析

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

  1. V = f(M)

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

  1. V + ΔV = f(M + ΔM)

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

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

  1. ΔM = perform (action)

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

  1. state = actions.reduce(reducer, initState)

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

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

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

数据流框架

Redux

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

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

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

  1. 定义过于繁琐,文件多,容易造成思维跳跃。

  2. 异步流的处理没有优雅的方案。

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

actions . js

  1. export const FETCH_DATA_START = 'FETCH_DATA_START';

  2. export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';

  3. export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR';


  4. export function fetchData() {

  5. return dispatch => {

  6. dispatch(fetchDataStart());

  7. axios.get('xxx').then((data) => {

  8. dispatch(fetchDataSuccess(data));

  9. }).catch((error) => {

  10. dispatch(fetchDataError(error));

  11. });

  12. };

  13. }


  14. export function fetchDataStart() {

  15. return {

  16. type: FETCH_DATA_START,

  17. }

  18. }


  19. ...FETCH_DATA_SUCCESS

  20. ...FETCH_DATA_ERROR

reducer . js

  1. import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR } from 'actions.js';

  2. export default (state = { data: null }, action) => {

  3. switch (action.type) {

  4. case FETCH_DATA_START:

  5. ...

  6. case FETCH_DATA_SUCCESS:

  7. ...

  8. case FETCH_DATA_ERROR:

  9. ...

  10. default:

  11. return state

  12. }

  13. }

view . js

  1. import { createStore, applyMiddleware } from 'redux';

  2. import thunk from 'redux-thunk';

  3. import reducer from 'reducer.js';

  4. import { fetchData } from 'actions.js';


  5. const store = createStore(reducer, applyMiddleware(thunk));

  6. store.dispatch(fetchData());

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

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

我觉得 Redux 设计上没有问题,思路非常简洁,是我非常喜欢的一个库,它提供的数据的流动方式,目前也是得到社区的广泛认可。然而在使用上有它的缺陷,虽然是可以克服,但是它本身难道没有可以优化的地方?






dva

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

model . js

  1. export default {

  2. namespace: 'products',

  3. state: [],

  4. reducers: {

  5. 'delete'(state, { payload: id }) {

  6. return state.filter(item => item.id !== id);

  7. },

  8. },

  9. };

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

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

model . js

  1. export default {

  2. namespace: 'counter',

  3. state: [],

  4. reducers: {

  5. },

  6. effects: {

  7. *add(action, { call, put }) {

  8. yield call(delay, 1000);

  9. yield put({ type: 'minus' });

  10. },

  11. },

  12. };

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

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

mirrorx

mirrorx 和 dva 差不多,只是它使用了单例的方式,所有的 action 都保存了 actions 对象中,访问 action 有了另一种方式。还有就是处理异步 action 的时候可以使用 async / await 的方式。

  1. import mirror, { actions } from 'mirrorx'


  2. mirror.model({

  3. name: 'app',

  4. initialState: 0,

  5. reducers: {

  6. increment(state) { return state + 1 },

  7. decrement(state) { return state - 1 }

  8. },

  9. effects: {

  10. async incrementAsync() {

  11. await new Promise((resolve, reject) => {

  12. setTimeout(() => {

  13. resolve()

  14. }, 1000)

  15. })

  16. actions.app.increment()

  17. }

  18. }

  19. });

它内部处理异步流的问题,类似 redux - thunk 的处理方式,通过注入一个中间件,这个中间件里判断 当前 action 是不是异步 action (只要判断是不是 effects 里定义的 action 即可),如果是的话,就直接中断了中间件的链式调用,可以看看这段代码。

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

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

xredux

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

使用上和 mirrorx 差不多,但它和 Redux 是一样的,不绑定任何框架,可以独立使用。

  1. import xredux from "xredux";


  2. const store = xredux.createStore();

  3. const actions = xredux.actions;


  4. // This is a model, a pure object with namespace, initialState, reducers, effects.

  5. xredux.model({

  6. namespace: "counter",

  7. initialState: 0,

  8. reducers: {

  9. add(state, action) { return state + 1; },

  10. plus(state, action) { return state - 1; },

  11. },

  12. effects: {

  13. async addAsync(action, dispatch, getState) {

  14. await new Promise(resolve => {

  15. setTimeout(() => {

  16. resolve();

  17. }, 1000);

  18. });

  19. actions.counter.add();

  20. }

  21. }

  22. });


  23. // Dispatch action with xredux.actions

  24. actions.counter.add();

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

  1. import xredux from 'xredux';

  2. import { fetchUserInfo } from 'services/api';


  3. const { actions } = xredux;


  4. xredux.model({

  5. namespace: 'user',

  6. initialState: {

  7. getUserInfoStart: false,

  8. getUserInfoError: null,

  9. userInfo: null,

  10. },

  11. reducers: {

  12. // fetch start

  13. getUserInfoStart (state, action) {

  14. return {

  15. ...state,

  16. getUserInfoStart: true,

  17. };

  18. },

  19. // fetch error

  20. getUserInfoError (state, action) {

  21. return {

  22. ...state,

  23. getUserInfoStart: false,

  24. getUserInfoError







请到「今天看啥」查看全文