根据文档,“没有中间件,Redux商店只支持同步数据流”。我不明白为什么会这样。为什么容器组件不能调用异步API,然后分派操作?

例如,想象一个简单的UI:一个字段和一个按钮。当用户按下按钮时,该字段将填充来自远程服务器的数据。

import * as React from 'react';
import * as Redux from 'redux';
import { Provider, connect } from 'react-redux';

const ActionTypes = {
    STARTED_UPDATING: 'STARTED_UPDATING',
    UPDATED: 'UPDATED'
};

class AsyncApi {
    static getFieldValue() {
        const promise = new Promise((resolve) => {
            setTimeout(() => {
                resolve(Math.floor(Math.random() * 100));
            }, 1000);
        });
        return promise;
    }
}

class App extends React.Component {
    render() {
        return (
            <div>
                <input value={this.props.field}/>
                <button disabled={this.props.isWaiting} onClick={this.props.update}>Fetch</button>
                {this.props.isWaiting && <div>Waiting...</div>}
            </div>
        );
    }
}
App.propTypes = {
    dispatch: React.PropTypes.func,
    field: React.PropTypes.any,
    isWaiting: React.PropTypes.bool
};

const reducer = (state = { field: 'No data', isWaiting: false }, action) => {
    switch (action.type) {
        case ActionTypes.STARTED_UPDATING:
            return { ...state, isWaiting: true };
        case ActionTypes.UPDATED:
            return { ...state, isWaiting: false, field: action.payload };
        default:
            return state;
    }
};
const store = Redux.createStore(reducer);
const ConnectedApp = connect(
    (state) => {
        return { ...state };
    },
    (dispatch) => {
        return {
            update: () => {
                dispatch({
                    type: ActionTypes.STARTED_UPDATING
                });
                AsyncApi.getFieldValue()
                    .then(result => dispatch({
                        type: ActionTypes.UPDATED,
                        payload: result
                    }));
            }
        };
    })(App);
export default class extends React.Component {
    render() {
        return <Provider store={store}><ConnectedApp/></Provider>;
    }
}

渲染导出的组件时,我可以单击该按钮,输入将正确更新。

注意connect调用中的update函数。它发送一个动作,告诉应用程序它正在更新,然后执行一个异步调用。调用完成后,所提供的值将作为另一个操作的有效负载进行调度。

这种方法有什么问题?为什么我要使用Redux Thunk或Redux Promise,如文档所示?

编辑:我在Redux repo中搜索线索,发现Action Creator在过去被要求是纯函数。例如,这里有一个用户试图为异步数据流提供更好的解释:

动作创建者本身仍然是一个纯函数,但它返回的thunk函数不需要是纯函数,它可以执行我们的异步调用

动作创作者不再需要纯粹。因此,thunk/promise中间件在过去肯定是必需的,但现在似乎不再是这样了?


当前回答

这种方法有什么问题?为什么我要使用Redux Thunk或Redux Promise,如文档所示?

这种方法没有错。在大型应用程序中,这是不方便的,因为您将有不同的组件执行相同的操作,您可能希望取消某些操作,或保持某些本地状态,如自动递增ID靠近操作创建者等。因此,从维护的角度来看,将操作创建者提取到单独的函数中更容易。

您可以阅读我对“如何在超时情况下调度Redux操作”的回答,以了解更详细的演练。

像Redux Thunk或Redux Promise这样的中间件只是为你提供了发送Thunk或Promise的“语法糖”,但你不必使用它。

因此,如果没有任何中间件,您的动作创建者可能看起来像

// action creator
function loadData(dispatch, userId) { // needs to dispatch, so it is first argument
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  loadData(this.props.dispatch, this.props.userId); // don't forget to pass dispatch
}

但使用Thunk中间件,您可以这样写:

// action creator
function loadData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`) // Redux Thunk handles these
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_DATA_FAILURE', err })
    );
}

// component
componentWillMount() {
  this.props.dispatch(loadData(this.props.userId)); // dispatch like you usually do
}

所以没有什么大的区别。我喜欢后一种方法的一点是,组件不关心动作创建者是异步的。它只是正常调用dispatch,它还可以使用mapDispatchToProps以短语法等方式绑定此类动作创建者。组件不知道动作创建者是如何实现的,您可以在不同的异步方法(Redux Thunk、Redux Promise、Redux Saga)之间切换,而无需更改组件。另一方面,使用前一种显式方法,您的组件确切地知道特定调用是异步的,并且需要通过某种约定(例如,作为同步参数)传递分派。

还请考虑此代码将如何更改。假设我们希望有第二个数据加载函数,并将它们组合在一个动作创建器中。

对于第一种方法,我们需要注意我们所称的行动创造者:

// action creators
function loadSomeData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(dispatch, userId) {
  return fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(dispatch, userId) {
  return Promise.all(
    loadSomeData(dispatch, userId), // pass dispatch first: it's async
    loadOtherData(dispatch, userId) // pass dispatch first: it's async
  );
}


// component
componentWillMount() {
  loadAllData(this.props.dispatch, this.props.userId); // pass dispatch first
}

使用Redux Thunk,动作创建者可以调度其他动作创建者的结果,甚至不考虑这些结果是同步的还是异步的:

// action creators
function loadSomeData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
    );
}
function loadOtherData(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'LOAD_OTHER_DATA_SUCCESS', data }),
      err => dispatch({ type: 'LOAD_OTHER_DATA_FAILURE', err })
    );
}
function loadAllData(userId) {
  return dispatch => Promise.all(
    dispatch(loadSomeData(userId)), // just dispatch normally!
    dispatch(loadOtherData(userId)) // just dispatch normally!
  );
}


// component
componentWillMount() {
  this.props.dispatch(loadAllData(this.props.userId)); // just dispatch normally!
}

使用这种方法,如果您希望动作创建者查看当前的Redux状态,您可以只使用传递给thunks的第二个getState参数,而不必修改调用代码:

function loadSomeData(userId) {
  // Thanks to Redux Thunk I can use getState() here without changing callers
  return (dispatch, getState) => {
    if (getState().data[userId].isLoaded) {
      return Promise.resolve();
    }

    fetch(`http://data.com/${userId}`)
      .then(res => res.json())
      .then(
        data => dispatch({ type: 'LOAD_SOME_DATA_SUCCESS', data }),
        err => dispatch({ type: 'LOAD_SOME_DATA_FAILURE', err })
      );
  }
}

如果需要将其更改为同步,也可以在不更改任何调用代码的情况下执行此操作:

// I can change it to be a regular action creator without touching callers
function loadSomeData(userId) {
  return {
    type: 'LOAD_SOME_DATA_SUCCESS',
    data: localStorage.getItem('my-data')
  }
}

因此,使用Redux Thunk或Redux Promise等中间件的好处是,组件不知道动作创建者是如何实现的,也不知道它们是否关心Redux状态,它们是同步还是异步,以及它们是否调用其他动作创建者。缺点是有点间接,但我们认为这在实际应用中是值得的。

最后,Redux Thunk和朋友只是Redux应用程序中异步请求的一种可能方法。另一种有趣的方法是Redux Saga,它允许您定义长时间运行的守护进程(“sagas”),这些守护进程在执行操作时执行操作,并在输出操作之前转换或执行请求。这将逻辑从动作创作者转变为传奇。你可能想看看,然后选择最适合你的。

我在Redux repo中搜索线索,发现Action Creator在过去被要求是纯函数。

这是不正确的。医生们这么说,但医生们错了。动作创建者从未被要求是纯函数。我们修改了文档以反映这一点。

其他回答

简短的回答:对我来说,这似乎是解决异步问题的一种完全合理的方法。

当我在一个新项目上工作时,我有一个非常相似的想法,我们刚刚开始我的工作。我是vanilla Redux优雅系统的忠实粉丝,该系统可以更新商店并以一种远离React组件树的方式重新渲染组件。在我看来,使用这种优雅的调度机制来处理异步似乎很奇怪。

我最终采用了一种非常类似的方法,就像我从项目中分解出来的一个库一样,我们称之为react redux控制器。

出于以下几个原因,我最终没有采用上述方法:

按照您编写的方式,这些调度功能无法访问商店。通过让UI组件传递调度函数所需的所有信息,您可以在某种程度上绕过这个问题。但我认为这将不必要地将这些UI组件耦合到调度逻辑。更有问题的是,调度函数没有明显的方法来访问异步延续中的更新状态。分派函数可以通过词法范围访问分派本身。一旦connect语句失控,这就限制了重构的选项——而且仅使用一个更新方法看起来非常笨拙。因此,如果您将这些分派器函数分解为单独的模块,您需要一些系统来组合它们。

总之,您必须装配一些系统,以便将调度和存储以及事件的参数注入到调度函数中。我知道三种合理的依赖注入方法:

redux thunk通过将它们传递给你的thunk(根据dome定义,使它们完全不是thunk),以一种功能性的方式实现了这一点。我还没有使用过其他调度中间件方法,但我认为它们基本相同。react-redux控制器通过协程实现这一点。另外,它还允许您访问“选择器”,这些函数可能是您作为连接的第一个参数传入的,而不必直接使用原始的标准化存储。您还可以通过各种可能的机制将它们注入到这个上下文中,以面向对象的方式来实现这一点。

使现代化

我想到这个难题的一部分是react redux的限制。connect的第一个参数获取状态快照,但不获取分派。第二个参数得到分派,而不是状态。这两个参数都没有获得关闭当前状态的thunk,因为在继续/回调时能够看到更新的状态。

好了,让我们先来看看中间件是如何工作的,这很好地回答了这个问题,这是Redux中的源代码applyMiddleWare函数:

function applyMiddleware() {
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }

  return function (createStore) {
    return function (reducer, preloadedState, enhancer) {
      var store = createStore(reducer, preloadedState, enhancer);
      var _dispatch = store.dispatch;
      var chain = [];

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch(action) {
          return _dispatch(action);
        }
      };
      chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      _dispatch = compose.apply(undefined, chain)(store.dispatch);

      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

看看这一部分,看看我们的调度是如何成为一个函数的。

  ...
  getState: store.getState,
  dispatch: function dispatch(action) {
  return _dispatch(action);
}

请注意,每个中间件都将作为命名参数提供dispatch和getState函数。

好了,这就是Redux thunk作为Redux最常用的中间件之一介绍自己的方式:

Redux Thunk中间件允许您编写返回函数而不是动作。thunk可用于延迟动作的调度,或仅在某个条件为遇见。内部函数接收存储方法分派和getState作为参数。

正如你所看到的,它将返回一个函数而不是一个操作,这意味着你可以随时等待并调用它,因为它是一个函数。。。

那他妈的是什么?维基百科就是这样介绍的:

在计算机编程中,thunk是一种用于注入将附加计算转换为另一个子例程。雷电主要是用于将计算延迟到需要时,或插入在另一个子例程的开始或结束处的操作。他们有用于编译器代码生成和模块化编程。这个词起源于“思考”的一个滑稽派生词。thunk是一个包装表达式以延迟其评价

//calculation of 1 + 2 is immediate 
//x === 3 
let x = 1 + 2;

//calculation of 1 + 2 is delayed 
//foo can be called later to perform the calculation 
//foo is a thunk! 
let foo = () => 1 + 2;

所以,看看这个概念有多简单,它如何帮助您管理异步操作。。。

这是你可以不用它生活的东西,但请记住,在编程中,总是有更好、更整洁和正确的方式来做事情。。。

要回答开头提出的问题:

为什么容器组件不能调用异步API,然后分派操作?

请记住,这些文档适用于Redux,而不是Redux+React。连接到React组件的Redux商店可以做到您所说的一切,但没有中间件的Plain Jane Redux商店不接受除普通对象之外的参数来分派。

如果没有中间件,你当然还可以

const store = createStore(reducer);
MyAPI.doThing().then(resp => store.dispatch(...));

但这是一个类似的情况,异步是围绕着Redux而不是由Redux处理的。因此,中间件通过修改可以直接传递给调度的内容来允许异步。


也就是说,我认为你的建议的精神是有效的。在Redux+Rreact应用程序中,当然还有其他方法可以处理异步。

使用中间件的一个好处是,您可以继续正常使用动作创建者,而不必担心它们是如何连接的。例如,使用redux thunk,您编写的代码看起来很像

function updateThing() {
  return dispatch => {
    dispatch({
      type: ActionTypes.STARTED_UPDATING
    });
    AsyncApi.getFieldValue()
      .then(result => dispatch({
        type: ActionTypes.UPDATED,
        payload: result
      }));
  }
}

const ConnectedApp = connect(
  (state) => { ...state },
  { update: updateThing }
)(App);

它看起来与原始版本没有太大区别-只是有点混乱-connect不知道updateThing是(或需要)异步的。

如果您还想支持承诺、可观测性、传奇或疯狂的自定义和高度声明性的动作创建者,那么Redux可以通过更改传递给分派的内容(也就是从动作创建者返回的内容)来实现。无需干扰React组件(或连接调用)。

阿布拉莫夫的目标(也是每个人的理想目标)只是将复杂性(和异步调用)封装在最合适和可重用的地方。

在标准的Redux数据流中,哪里是最好的地方?怎么样:

减速器?不可能。它们应该是没有副作用的纯函数。更新商店是一项严肃而复杂的业务。不要污染它。哑视图组件?肯定不会。他们有一个问题:演示和用户交互,并且应该尽可能简单。容器组件?可能,但次优。这是有意义的,因为容器是我们封装一些视图相关复杂性并与存储交互的地方,但是:容器确实需要比哑组件更复杂,但它仍然是一个单一的责任:在视图和状态/存储之间提供绑定。您的异步逻辑与此完全不同。通过将其放置在容器中,可以将异步逻辑锁定到一个上下文中,并与一个或多个视图/路由相耦合。坏主意。理想情况下,它都是可重用的,并且与视图完全分离。(与所有规则一样,如果您有状态绑定逻辑,恰好可以在多个上下文中重用,或者您可以以某种方式将所有状态概括为集成的GraphQL模式,则可能会出现异常。好吧,很好,这可能很酷。但……大多数时候,绑定似乎都是上下文/视图特定的。)其他服务模块?坏主意:您需要注入对存储的访问,这是一个可维护性/可测试性噩梦。最好使用Redux的颗粒,只使用提供的API/模型访问商店。动作和解释它们的中间件?为什么不呢?首先,这是我们剩下的唯一主要选择从逻辑上讲,操作系统是解耦的执行逻辑,您可以在任何地方使用它。它可以访问商店,并可以调度更多操作。它有一个单一的职责,即组织应用程序周围的控制和数据流,而大多数异步正好适合这一点。动作创造者呢?为什么不在那里做异步,而不是在操作本身和中间件中?首先,也是最重要的一点,创建者不能像中间件那样访问存储。这意味着你不能分派新的临时动作,不能从存储中读取以组成异步等等。所以,把复杂性放在一个必要性复杂的地方,把其他一切都保持简单。然后,创建者可以是简单的、相对简单的、易于测试的函数。

当在企业项目中工作时,中间件中有许多可用的需求,例如(saga)在简单的异步流中不可用,下面是一些:

并行运行请求无需等待,即可采取未来行动无阻塞呼叫比赛效果,示例先拾取响应以启动流程为您的任务排序(首次呼叫)合成任务取消动态分叉任务。支持redux中间件之外的并发运行Saga。使用频道

列表很长,只需查看saga文档中的高级部分