当前位置: 首页 > 知识库问答 >
问题:

为什么我们需要中间件用于Redux中的异步流?

须鸿祯
2023-03-14

根据文档,“没有中间件,Redux store只支持同步数据流”。我不明白为什么会这样。为什么容器组件不能调用异步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>;
    }
}

当导出的组件呈现时,我可以单击按钮,输入就正确更新了。

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

这种做法有何不妥?为什么我要使用Redux Thunk或Redux Promise,正如文档所建议的那样?

编辑:我在Redux repo上搜索线索,发现过去要求动作创建者是纯函数。例如,以下是一个用户试图为异步数据流提供更好的解释:

action creator本身仍然是一个纯函数,但是它返回的thunk函数不需要是纯函数,它可以执行异步调用

不再要求行动创造者是纯洁的。那么,过去肯定需要使用Thunk/Promise中间件,但现在似乎不再是这样了?

共有3个答案

金钊
2023-03-14

简短的回答:在我看来,这是解决异步问题的一种完全合理的方法。但有几个注意事项。

在我的工作中,我们刚开始做一个新项目时,我也有非常相似的想法。我是vanilla Redux优雅的系统的忠实粉丝,该系统用于更新存储区并以一种不受React组件树的影响的方式重新生成组件。在我看来,钩入优雅的 机制来处理异步是很奇怪的。

最后,我采用了一种非常类似的方法,在我们的项目中,我们把它称为react-redux-controller。

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

    本身。一旦 语句失控,这就限制了重构的选项--而且仅用一个 方法看起来非常笨拙。因此,如果将这些分派器函数分解为单独的模块,则需要一些系统来使它们能够组合。/LI>

总之,您必须安装一些系统,允许将 和存储以及事件参数注入到分派函数中。对于这种依赖注入,我知道三种合理的方法:

    中间件方法,但我认为它们基本上是相同的。/li> 的函数,而不必直接处理原始的,规范化的store./li>
  • 您也可以通过各种可能的机制将它们注入 上下文中,从而以面向对象的方式完成此操作。/li>
  • selectors”,这是您可能作为第一个参数传递给

更新

在我看来,这个难题的一部分是反应还原的限制。 的第一个参数获取状态快照,但不获取dispatch。第二个参数获得dispatch,但不获得state。这两个参数都没有一个在当前状态上关闭的thunk,因为它能够在continuation/callback时看到更新的状态。

李光华
2023-03-14

Dan Abramov 的回答是正确的,但我将更多地谈谈redux-saga,它非常类似,但更强大。

    是命令式的/ 是声明式的/li>

当你手中有一个thunk,比如IO单子或一个诺言,你不容易知道一旦你执行它会做什么。测试thunk的唯一方法是执行它,并模拟分派程序(或者模拟整个外部世界,如果它与更多的填充物交互。。。。。)。

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

从附带效应的角度来看,mocks是代码不纯的标志,在函数式程序员的眼中,则是有问题的证明。与其下载一个库来帮助我们检查冰山是否完好,不如绕着它航行。一个TDD/Java的铁杆家伙曾经问过我你是如何在Clojure中进行嘲讽的。答案是,我们通常不会。我们通常把它看作是我们需要重构代码的标志。

来源

SAGA(在 中实现)是声明性的,与免费的monad或React组件一样,它们更易于测试,无需任何模拟。

另见本文:

在现代FP中,我们不应该写程序,而应该写程序的描述,然后我们可以随意地反思,转换和解释程序。

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

在前端世界中,对于一些后端概念(如CQRS/EventSourcing和Flux/Redux)之间的关系有很多困惑,主要是因为在Flux/Flux/Redux中,我们使用术语“action”,它有时可以表示命令式代码(codeload_user/code>)和事件(codeuser_loaded/code)。我相信,与事件源一样,您应该只分派事件。

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

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

这个传奇故事的意思是:

每次单击用户名时,获取用户配置文件,然后用加载的配置文件分派一个事件。

正如您所看到的, 有一些优点。

使用 可以表示您只想获得上次点击的用户名的数据(处理并发问题,以防用户快速点击很多用户名)。这种东西用thunks是硬的。如果不想要这种行为,可以使用

你让行动创造者保持纯洁。注意,保留actionCreators(在sagas 和components

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

您不再需要触发类似于RPC的调用,如 。您的UI只需要分派已经发生的事情。我们只触发事件(总是在过去时态!)而不再是行动。这意味着您可以创建解耦的“鸭子”或有界的上下文,并且saga可以充当这些模块化组件之间的耦合点。

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

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

有些人可能会争辩说,生成器可以固有地用局部变量隐藏redux存储外部的状态,但是如果您开始通过启动计时器等在thunk内部编排复杂的东西,那么您无论如何都会遇到同样的问题。还有一个 效果,它现在允许从Redux存储中获取一些状态。

Sagas可以穿越时间,还支持复杂的流日志记录和当前正在开发的开发工具。下面是一些已经实现的简单异步流日志记录:

传奇故事不仅取代了redux Thunks。它们来自后端/分布式系统/事件源。

这是一个很常见的误解,认为saga只是在这里用更好的可测试性来取代你的redux thunk。实际上,这只是Redux-Saga的一个实现细节。在可测试性方面,使用声明性效果比thunk更好,但是saga模式可以在命令式或声明性代码之上实现。

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

为了简化前端世界,假设有widget1和widget2。当widget1上的某个按钮被单击时,它应该会对Widget2产生影响。widget1不是将两个小部件耦合在一起(即widget1分派一个针对widget2的动作),而是只分派它的按钮被单击。然后,saga侦听这个按钮,单击,然后通过释放widget2知道的新事件来更新widget2。

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

关于如何构造Redux应用程序的一些不错的文章,您可以在这些应用程序上使用Redux-saga来解耦:

我希望我的组件能够触发应用程序内通知的显示。但是我不希望我的组件高度耦合到具有自己的业务规则(最多同时显示3个通知,通知排队,4秒显示时间等等)的通知系统。

我不希望我的JSX组件决定何时显示/隐藏通知。我只是赋予它请求通知的能力,而把复杂的规则留在传奇中。这种东西很难用thunks或承诺来实现。

我已经在这里描述了如何用saga来实现这一点

saga这个术语来自后端世界。我最初是在一次长时间的讨论中向Yassine(Redux-saga的作者)介绍这个术语的。

最初,这个术语是在一篇论文中引入的,saga模式被认为是用来处理分布式事务中的最终一致性的,但是它的用法被后端开发人员扩展到了一个更广泛的定义,因此它现在也涵盖了“流程管理器”模式(不知何故,最初的saga模式是流程管理器的一种专门形式)。

今天,“传奇”一词令人困惑,因为它可以描述两种不同的事物。由于它在redux-saga中使用,它并不描述处理分布式事务的方法,而是描述在应用程序中协调操作的方法。 也可以称为

另请参阅:

    《关于还原传奇史的亚辛访谈录》

如果您不喜欢使用生成器的想法,但是您对saga模式及其解耦特性感兴趣,那么您也可以使用redux-observable实现相同的功能,redux-observable使用名称 来描述完全相同的模式,但是使用RXJS。如果你已经熟悉了Rx,你就会有宾至如归的感觉。

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
        }))
    );
  • 从actionsCreators到SAGAS/li> <用Redux-Saga/Li实现的LiSnake游戏>
    <不要仅仅为了使用Redux-saga而过度使用Redux-saga。只调用可测试的API是不值得的。/li>
  • 对于大多数简单情况,不从项目中删除thunks./li> 中分派thunk(如果有意义的话),不要犹豫。/li>

如果您害怕使用Redux-saga(或Redux-observable),但只需要解耦模式,请检查redux-dispatch-subscribe:它允许侦听分派并在侦听器中触发新的分派。

const unsubscribe = store.addDispatchListener(action => {
  if (action.type === 'ping') {
    store.dispatch({ type: 'pong' });
  }
});
井斌斌
2023-03-14

这种做法有何不妥?为什么我要使用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)而不需要改变组件。另一方面,使用前一种显式方法,您的组件确切地知道特定调用是异步的,并且需要 通过某种约定(例如,作为sync参数)传递。

还要考虑一下这段代码将如何变化。假设我们想要第二个数据加载函数,并将它们组合在一个操作创建者中。

对于第一种方法,我们需要注意我们调用的是哪种操作创建者:

// 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状态,您可以只使用传递给thunk的第二个 参数,而完全不修改调用代码:

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,它允许您定义长时间运行的守护进程(long-running daemons,Saga),它在操作到来时采取操作,并在输出操作之前转换或执行请求。这将逻辑从动作创建者转移到传奇故事中。你可能会想要检查一下,然后选择最适合你的。

我在Redux repo上搜索线索,发现过去要求动作创建者是纯函数。

这是不正确的。文档这样说,但文档是错误的。br>操作创建者从来没有被要求是纯粹的功能。br>我们修复了文档以反映这一点。

 类似资料:
  • 问题内容: 我知道弱引用是垃圾收集器的摆布,我们不能保证弱引用会存在。我认为没有必要提供较弱的参考,但可以确定应该有一个理由。 为什么我们需要Java中的弱引用? Java中弱引用的实际(某些)用法是什么?如果您可以分享您在项目中的使用方式,那就太好了! 问题答案: 使用弱哈希图实际上通常是一个坏主意。首先,很容易出错,但更糟糕的是,它通常用于实现某种缓存。 这意味着以下内容:您的程序在一段时间内

  • 问题内容: 我将稍微解释一下我的脚本,以便您可以理解我的问题。 基本上我做了一个脚本来检查SOCKS5是还是。 当我在上面测试我的脚本时,它运行良好,但是当我在Windows上对其进行测试时,直到我将以下行添加到: 谁能向我解释为什么我在Windows中需要此行,而在Linux服务器上却不需要? 问题答案: SSL证书上的此cURL手册页介绍了连接到SSL / TLS受保护主机时 证书验证 的过程

  • 问题内容: 我开始使用RxJS,但我不明白为什么在此示例中我们需要使用类似or 的函数;数组的数组在哪里? 如果有人可以直观地解释正在发生的事情,那将非常有帮助。 问题答案: 当您有一个Observable的结果是更多Observable时,可以使用flatMap。 如果您有一个由另一个可观察对象产生的可观察对象,则您不能直接过滤,缩小或映射它,因为您有一个可观察对象而不是数据。如果您生成一个可观

  • 问题内容: 训练期间需要调用该方法。但是文档不是很有帮助 为什么我们需要调用此方法? 问题答案: 在中,我们需要在开始进行反向传播之前将梯度设置为零,因为PyTorch 会 在随后的向后传递中 累积梯度 。在训练RNN时这很方便。因此,默认操作是在每次调用时累积(即求和)梯度。 因此,理想情况下,当您开始训练循环时,应该正确进行参数更新。否则,梯度将指向预期方向以外的其他方向,即朝向 最小值 (或

  • 问题内容: Angular应用使用属性而不是事件。 为什么是这样? 问题答案: ng-click包含一个角度表达式。Angular表达式是在Angular 范围的上下文中求值的,该范围绑定到具有ng- click属性的元素或该元素的祖先。 Angular表达式语言不包含流控制语句,也不能声明变量或定义函数。这些限制意味着模板只能访问由控制器或指令提供的变量和运行功能。

  • 以我的拙见,关于“什么是单子”这个著名问题的答案,尤其是投票最多的答案,试图解释什么是单子,而没有明确解释为什么单子是真正必要的。它们能被解释为一个问题的解决方案吗?

  • 为什么我们需要字典? 计算机最适合使用数字,而人类最适合使用姓名。我们创建了DNS以便记住主机名而不是IP地址。字典以相同的方式使用,因此我们可以记住AVP名称而不是类型编号。当FreeRADIUS解析请求或生成响应时,会查阅字典。 但是,字典与DNS不同,因为RADIUS客户端不知道FreeRADIUS使用的这些“友好”名称。永远不会在RADIUS客户端和RADIUS服务器之间交换AVP名称。

  • 问题内容: 基数实际上是什么意思?我们为什么需要它? 问题答案: 您可能并不总是希望将整数解析为以10为底的数字,因此提供基数可以指定其他数字系统。 基数是一位数字的值数。十六进制为16。八进制为8,二进制为2,依此类推… 在该函数中,您可以执行一些操作来提示基数而不提供基数。如果用户输入的字符串与其中一个规则匹配,但没有明确规定,则这些方法也可能对您不利。例如: