为什么我们需要 Redux 中的异步流中间件?你没有。但。。。你应该使用redux-saga:)命令式 VS 声明式混乱:操作/事件/命令...在实践中使用传奇解耦A concrete usecase: notification systemWhy is it called a Saga?AlternativesSome redux-saga useful resources2017 advises

根据文档,“没有中间件,Redux存储仅支持同步数据流”。我不明白为什么会这样。为什么容器组件不能先调用异步 API,然后再调用操作?dispatch

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

A field and a button

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>;
    }
}

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

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

这种方法有什么问题?为什么我要使用 Redux Thunk 或 Redux Promise,正如文档所建议的那样?

编辑:我在 Redux 存储库中搜索线索,发现过去需要操作创建者是纯函数。例如,下面是一个用户试图为异步数据流提供更好的解释:

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

操作创建者不再需要纯正。所以,过去肯定是需要thunk/promise中间件的,但似乎现在情况已经不同了?


答案 1

这种方法有什么问题?为什么我要使用 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
}

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

还要考虑此代码将如何更改。假设我们想要第二个数据加载函数,并将它们组合在单个操作创建器中。

在第一种方法中,我们需要注意我们称之为什么样的行动创造者:

// 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,动作创建者可以产生其他动作创建者的结果,甚至不考虑这些是同步的还是异步的:dispatch

// 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 和 friends 只是 Redux 应用程序中异步请求的一种可能方法。另一个有趣的方法是 Redux Saga,它允许您定义长时间运行的守护进程(“sagas”),这些守护程序在操作发生时执行操作,并在输出操作之前转换或执行请求。这会将逻辑从动作创建者转移到传奇中。您可能想检查一下,然后选择最适合您的内容。

我在 Redux 存储库中搜索线索,发现过去需要操作创建者是纯函数。

这是不正确的。文档是这么说的,但文档是错的。
动作创建者从来不需要是纯函数。
我们修复了文档以反映这一点。


答案 2

你没有。

但。。。你应该使用redux-saga:)

Dan Abramov的答案是正确的,但我会更多地谈论一些类似但更强大的redux-sagaredux-thunk

命令式 VS 声明式

  • DOM:jQuery is imperative / React is declarative
  • Monads:IO是命令式的/Free是声明性的
  • Redux 效应:是命令式的/是声明性的redux-thunkredux-saga

当你手里有一个锤子,比如一个IO monad或一个承诺,你不能轻易知道一旦你执行了它会做什么。测试 thunk 的唯一方法是执行它,并嘲笑调度程序(或者如果它与更多东西交互,则嘲笑整个外部世界...

如果你使用的是模拟,那么你就不是在做函数式编程。

通过副作用的镜头来看,模拟是你的代码不纯的标志,在函数式程序员的眼中,证明有什么地方是错误的。与其下载一个图书馆来帮助我们检查冰山是否完好无损,我们应该在它周围航行。一个铁杆TDD/Java家伙曾经问我,你是如何在Clojure中嘲笑的。答案是,我们通常不会。我们通常将其视为需要重构代码的标志。

sagas(因为它们是在 中实现的)是声明性的,就像Free monad或React组件一样,它们更容易测试,没有任何模拟。redux-saga

另请参阅此文章

在现代FP中,我们不应该编写程序 - 我们应该编写程序的描述,然后我们可以随意内省,转换和解释。

(实际上,Redux-saga就像一个混合体:流程是命令性的,但效果是声明性的)

混乱:操作/事件/命令...

在前端世界中,对于一些后端概念(如CQRS / EventSourcing和Flux / Redux)如何相关存在很多困惑,主要是因为在Flux中我们使用术语“action”,它有时可以表示命令性代码()和事件()。我相信,就像事件溯源一样,你应该只调度事件。LOAD_USERUSER_LOADED

在实践中使用传奇

想象一下,一个应用程序具有指向用户配置文件的链接。使用每个中间件处理此问题的惯用方法是:

redux-thunk

<div onClick={e => dispatch(actions.loadUserProfile(123)}>Robert</div>

function loadUserProfile(userId) {
  return dispatch => fetch(`http://data.com/${userId}`)
    .then(res => res.json())
    .then(
      data => dispatch({ type: 'USER_PROFILE_LOADED', data }),
      err => dispatch({ type: 'USER_PROFILE_LOAD_FAILED', err })
    );
}

redux-saga

<div onClick={e => dispatch({ type: 'USER_NAME_CLICKED', payload: 123 })}>Robert</div>


function* loadUserProfileOnNameClick() {
  yield* takeLatest("USER_NAME_CLICKED", fetchUser);
}

function* fetchUser(action) {
  try {
    const userProfile = yield fetch(`http://data.com/${action.payload.userId }`)
    yield put({ type: 'USER_PROFILE_LOADED', userProfile })
  } 
  catch(err) {
    yield put({ type: 'USER_PROFILE_LOAD_FAILED', err })
  }
}

这个传奇故事翻译为:

每次单击用户名时,获取用户配置文件,然后使用加载的配置文件调度事件。

如您所见,有一些优点。redux-saga

许可证的使用表示您只对获取上次单击的用户名的数据感兴趣(在用户快速单击大量用户名的情况下处理并发问题)。这种东西很难用thunks。如果您不希望出现此行为,则可以使用。takeLatesttakeEvery

你让动作创作者保持纯洁。请注意,保留 actionCreators(在 sagas 和组件中)仍然很有用,因为它可能有助于您在将来添加操作验证(断言/流/类型脚本)。putdispatch

您的代码变得更加可测试,因为效果是声明性的

您不再需要触发类似 rpc 的调用,例如 .你的 UI 只需要调度发生了什么。我们只触发事件(总是过去时态!),而不是行动。这意味着您可以创建解耦的“鸭子”有界上下文,并且saga可以充当这些模块化组件之间的耦合点。actions.loadUser()

这意味着您的视图更易于管理,因为它们不再需要包含已发生的情况和应该发生的事情之间的转换层作为效果。

例如,想象一个无限滚动视图。 可以导致,但是可滚动容器是否真的有责任决定我们是否应该加载另一个页面?然后,他必须知道更复杂的事情,例如最后一页是否已成功加载,或者是否已经有一个页面尝试加载,或者是否没有更多的项目可供加载?我不这么认为:为了获得最大的可重用性,可滚动容器应该只描述它已经滚动。页面的加载是该滚动的“业务效果”CONTAINER_SCROLLEDNEXT_PAGE_LOADED

有些人可能会争辩说,生成器本身可以使用局部变量将状态隐藏在redux存储之外,但是如果您通过启动计时器等开始在thunks内部编排复杂事物,那么无论如何您都会遇到同样的问题。现在有一种效果允许从 Redux 商店中获取一些状态。select

Sagas可以进行时间旅行,还可以实现目前正在开发的复杂流日志记录和开发工具。以下是一些已实现的简单异步流日志记录:

saga flow logging

解耦

Sagas不仅取代了redux thunks。它们来自后端/分布式系统/事件采购。

一个非常普遍的误解是,sagas只是为了用更好的可测试性取代你的redux thunks。实际上,这只是 redux-saga 的一个实现细节。使用声明性效果比 thunks 更好,但 saga 模式可以在命令式或声明性代码之上实现。

首先,saga是一个软件,允许协调长时间运行的事务(最终一致性)和跨不同边界上下文的事务(域驱动的设计术语)。

为了简化前端世界的这一点,想象一下有widget1和widget2。当单击 widget1 上的某些按钮时,它应该会对 widget2 产生影响。widget1 不是将 2 个小部件耦合在一起(即 widget1 调度一个以 widget2 为目标的操作),widget1 只调度其按钮被单击。然后,saga 侦听此按钮单击,然后通过分离 widget2 知道的新事件来更新 widget2。

这增加了一个对于简单应用不必要的间接寻址级别,但可以更轻松地缩放复杂应用程序。现在,您可以将 widget1 和 widget2 发布到不同的 npm 存储库,这样它们就不必相互了解,而不必让它们共享全局操作注册表。这 2 个小部件现在是可以单独存在的有界上下文。它们不需要彼此保持一致,也可以在其他应用程序中重复使用。saga是两个小部件之间的耦合点,它们以有意义的方式协调它们,为您的企业服务。

关于如何构建 Redux 应用的一些不错的文章,出于解耦原因,您可以使用 Redux-saga:

A concrete usecase: notification system

I want my components to be able to trigger the display of in-app notifications. But I don't want my components to be highly coupled to the notification system that has its own business rules (max 3 notifications displayed at the same time, notification queueing, 4 seconds display-time etc...).

I don't want my JSX components to decide when a notification will show/hide. I just give it the ability to request a notification, and leave the complex rules inside the saga. This kind of stuff is quite hard to implement with thunks or promises.

notifications

I've described here how this can be done with saga

Why is it called a Saga?

The term saga comes from the backend world. I initially introduced Yassine (the author of Redux-saga) to that term in a long discussion.

Initially, that term was introduced with a paper, the saga pattern was supposed to be used to handle eventual consistency in distributed transactions, but its usage has been extended to a broader definition by backend developers so that it now also covers the "process manager" pattern (somehow the original saga pattern is a specialized form of process manager).

Today, the term "saga" is confusing as it can describe 2 different things. As it is used in redux-saga, it does not describe a way to handle distributed transactions but rather a way to coordinate actions in your app. could also have been called . redux-sagaredux-process-manager

See also:

Alternatives

If you don't like the idea of using generators but you are interested by the saga pattern and its decoupling properties, you can also achieve the same with redux-observable which uses the name to describe the exact same pattern, but with RxJS. If you're already familiar with Rx, you'll feel right at home.epic

const loadUserProfileOnNameClickEpic = action$ =>
  action$.ofType('USER_NAME_CLICKED')
    .switchMap(action =>
      Observable.ajax(`http://data.com/${action.payload.userId}`)
        .map(userProfile => ({
          type: 'USER_PROFILE_LOADED',
          userProfile
        }))
        .catch(err => Observable.of({
          type: 'USER_PROFILE_LOAD_FAILED',
          err
        }))
    );

Some redux-saga useful resources

2017 advises

  • Don't overuse Redux-saga just for the sake of using it. Testable API calls only are not worth it.
  • Don't remove thunks from your project for most simple cases.
  • Don't hesitate to dispatch thunks in if it makes sense.yield put(someActionThunk)

If you are frightened of using Redux-saga (or Redux-observable) but just need the decoupling pattern, check redux-dispatch-subscribe: it permits to listen to dispatches and trigger new dispatches in listener.

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});