Redux: 异步操作的替代方法

创建于 2015-12-27  ·  44评论  ·  资料来源: reduxjs/redux

我一直在探索在 redux 中完成异步操作的替代方法,我很感激其他人对我所做的事情可能有的任何评论。

为了说明我的方法,我在我的 redux 克隆中更改了异步示例: https :

通常,外部动作是通过使动作创建者异步来完成的。 在异步示例的情况下,fetchPosts 操作创建者会调度 REQUEST_POSTS 操作以指示请求的开始,一旦帖子从 api 返回,就会发送 RECEIVE_POSTS 操作。

在我的示例中,所有动作创建者都是同步的。 相反,有一个函数会根据状态返回当前应该发生的异步操作列表。 在此处查看我的示例: https :

doReactions 函数订阅存储,并通过启动或取消请求来确保当前发出的请求的实际状态与 doReactions 状态返回的状态相匹配。

那么有什么区别呢?

1) 反应函数是状态的纯函数。 这使得测试变得容易。
2)发出请求的实际逻辑更简单。 参见我的示例中的几行函数,与之前通过容器和动作创建者传播的各种逻辑部分。
3) 使取消请求变得容易。

有什么想法吗?

discussion feedback wanted

最有用的评论

我也一直在思考很多关于在 redux 中处理副作用的替代方法,我希望我不会劫持你的线程,因为我大脑转储了我在一些当前方法中看到的一些问题,以及为什么和我认为这是尽管它看起来很简单,但朝着正确方向迈出了一大步。

动作创建者的副作用问题

在纯函数式语言中,副作用总是被提升到应用程序的边缘并返回到运行时执行。 在 Elm 中,reducers 返回一个包含新状态和任何应该执行的效果的元组。 但是,具有此签名的方法尚不能与其他 redux reducer 组合。

在 redux 中执行副作用的明显(但可能不是最好的)地方已经成为动作创建者,并且已经开发了几种不同的中间件选项来支持这种模式。 但是,我有点认为当前的中间件方法更像是一种解决方法,因为它无法将副作用作为减速器的一流概念返回。

虽然人们仍然在用 redux 构建很棒的东西,而且它比大多数替代方案向前迈出了一大步,而且更简单、更实用,但我看到在动作创建者内部产生副作用时存在一些问题:

  • 隐式状态被隐藏
  • 业务逻辑重复
  • 上下文假设和/或依赖性降低了可重用性
  • 有副作用的动作创建者很难测试
  • 无法优化或批处理

隐式状态被隐藏

在计数器应用程序 incrementAsync 中创建一个超时,只有在它完成时才会更新应用程序状态。 例如,如果您想显示一个可视指示器,指示正在进行增量操作,则视图无法从应用程序状态推断出这一点。 这种状态是隐式和隐藏的。

虽然有时很优雅,但我不太确定使用生成器作为动作创建者的协调者的建议,因为隐式状态是隐藏的,不能轻易序列化。

使用 redux-thunk 或类似工具,您可以向 reducer 发送多条消息,通知它何时开始增量操作以及何时完成,但这会产生一系列不同的问题。

在效果完成后将状态倒退到增量操作被标记为进行中的点实际上不会重新生成副作用,因此将无限期地保持进行中。

你的提议似乎解决了这个问题。 由于副作用是从状态产生的,因此意图必须以某种形式用结果状态表达,因此如果将存储恢复到启动动作的先前状态,那么反应将再次启动效果而不是让国家陷入困境。

业务逻辑重复

只有当应用程序处于特定状态时,动作才会产生副作用是很自然的。 在 redux 中,如果动作创建者需要状态,它必须是一个简单而纯的函数或明确提供状态。

作为一个简单的例子,假设我们从示例计数器应用程序开始,我们希望每次计数器是 5 的倍数时将计数器更改为随机字体颜色。

由于随机数生成是不纯的,建议将此行为放在动作创建者中。 然而,有几种不同的动作可以改变计数器的值,增量、减量、增量Async、增量IfOdd(在这种情况下不需要修改)。

increment 和 decrement 以前不需要任何状态,因为它们以前在 reducer 中处理过,因此可以访问当前值,但是由于 reducer 不能具有或返回副作用(随机数生成),这些函数现在成为需要的不纯动作创建者了解当前计数器值以确定是否需要选择新的随机字体颜色,并且该逻辑需要在所有计数器操作创建器中复制。

显式提供当前状态的一种可能替代方法是使用 redux-thunk 并返回回调以访问当前状态。 这允许您避免修改创建操作的所有位置以提供当前值,但现在要求操作创建者知道该值在全局应用程序状态中的存储位置,这限制了在其中多次重用相同计数器的能力相同的应用程序或不同的应用程序,其中状态的结构可能不同。

上下文假设和/或依赖性降低了可重用性

再次回顾反例,您会注意到只有一个反例。 虽然页面上有许多计数器来查看/更新相同的状态是微不足道的,但如果您希望每个计数器使用不同的状态,则需要对计数器进行额外的修改。

这在如何创建通用列表作为reducer 和组件增强器之前已经讨论过

如果计数器只使用简单的动作类型,那么应用elm 架构就相对简单了。

在这种情况下,父级简单地包装动作创建者或调度器以使用任何必要的上下文来扩充消息,然后它可以直接使用本地化状态调用化简器。

虽然React Elmish 示例看起来令人印象深刻,但该示例中明显缺少两个有问题的动作创建器,incrementIfOdd 和 incrementAsync。

incrementIfOdd 依赖中间件来确定当前状态,因此需要知道它在应用程序状态中的位置。

incrementAsync 最终会直接调度一个不暴露给父组件的增量操作,因此不能用额外的上下文包装。

虽然您的提议没有直接解决这个问题,但如果 incrementAsync 被实现为一个简单的操作,将状态更改为{counter: 0, incrementAfterDelay: 1000}以触​​发商店侦听器中的副作用,那么 incrementAsync 就变成了一条简单的消息。 incrementIfOdd 是纯的,所以它既可以在减速器中实现,也可以显式提供状态......因此,如果需要,可以再次应用 elm 架构。

有副作用的动作创建者很难测试

我认为这很明显,副作用将更难测试。 一旦您的副作用以当前状态和业务逻辑为条件,它们不仅变得更加难以测试,而且变得更加重要。

您的提议使人们能够轻松编写一个测试,即状态转换将创建一个包含所需反应的状态,而无需实际执行任何反应。 反应也更容易测试,因为它们不需要任何条件状态或业务逻辑。

无法优化或批处理

John A De Goes 最近的一篇博客文章讨论了不透明数据类型(例如 IO 或 Task)用于表达效果的问题。 通过使用副作用的声明性描述而不是不透明类型,您有可能在以后优化或组合效果。

FP 的现代架构

Thunk、promise 和生成器是不透明的,因此必须使用类似于fetchPostsIfNeeded函数显式处理批处理和/或抑制重复的 api 调用等优化。

您的提议消除了fetchPostsIfNeeded并且实现reactions函数似乎完全可行,该函数可以优化多个请求和/或在请求更多或更少数据时根据需要使用不同的 api 集。

我的实现

我最近创建了一个redux

我不知道如何在不分叉 redux 的情况下做到这一点,因为有必要修改 compose 和 combineReducers 以提升对现有减速器的影响,以保持与现有减速器代码的兼容性。

但是,您的建议非常好,因为它不需要修改 redux。 此外,我认为您的解决方案在解决隐含状态问题方面做得更好,并且可能更容易组合或优化结果反应。

概括

就像 React 是“只是 ui”,并没有非常明确地说明如何实际存储或更新应用程序状态,Redux 主要是“只是存储”,并且对于如何处理副作用并没有非常明确的规定。

我从来没有因为务实和完成工作而责怪任何人,而且 redux 和中间件的许多贡献者使人们能够比以前更快更好地构建真正酷的东西。 正是由于他们的贡献,我们才走到了这一步。 特别感谢所有做出贡献的人。

Redux 很棒。 这些不是 Redux 本身的必要问题,但希望对当前架构模式以及在状态修改之后而不是之前运行效果的动机和潜在优势提出建设性的批评。

所有44条评论

有趣的做法! 理想情况下,它似乎应该与异步的本质分离,例如用于运行 XHR 的方法,甚至 Web 请求首先是异步的来源。

我也一直在思考很多关于在 redux 中处理副作用的替代方法,我希望我不会劫持你的线程,因为我大脑转储了我在一些当前方法中看到的一些问题,以及为什么和我认为这是尽管它看起来很简单,但朝着正确方向迈出了一大步。

动作创建者的副作用问题

在纯函数式语言中,副作用总是被提升到应用程序的边缘并返回到运行时执行。 在 Elm 中,reducers 返回一个包含新状态和任何应该执行的效果的元组。 但是,具有此签名的方法尚不能与其他 redux reducer 组合。

在 redux 中执行副作用的明显(但可能不是最好的)地方已经成为动作创建者,并且已经开发了几种不同的中间件选项来支持这种模式。 但是,我有点认为当前的中间件方法更像是一种解决方法,因为它无法将副作用作为减速器的一流概念返回。

虽然人们仍然在用 redux 构建很棒的东西,而且它比大多数替代方案向前迈出了一大步,而且更简单、更实用,但我看到在动作创建者内部产生副作用时存在一些问题:

  • 隐式状态被隐藏
  • 业务逻辑重复
  • 上下文假设和/或依赖性降低了可重用性
  • 有副作用的动作创建者很难测试
  • 无法优化或批处理

隐式状态被隐藏

在计数器应用程序 incrementAsync 中创建一个超时,只有在它完成时才会更新应用程序状态。 例如,如果您想显示一个可视指示器,指示正在进行增量操作,则视图无法从应用程序状态推断出这一点。 这种状态是隐式和隐藏的。

虽然有时很优雅,但我不太确定使用生成器作为动作创建者的协调者的建议,因为隐式状态是隐藏的,不能轻易序列化。

使用 redux-thunk 或类似工具,您可以向 reducer 发送多条消息,通知它何时开始增量操作以及何时完成,但这会产生一系列不同的问题。

在效果完成后将状态倒退到增量操作被标记为进行中的点实际上不会重新生成副作用,因此将无限期地保持进行中。

你的提议似乎解决了这个问题。 由于副作用是从状态产生的,因此意图必须以某种形式用结果状态表达,因此如果将存储恢复到启动动作的先前状态,那么反应将再次启动效果而不是让国家陷入困境。

业务逻辑重复

只有当应用程序处于特定状态时,动作才会产生副作用是很自然的。 在 redux 中,如果动作创建者需要状态,它必须是一个简单而纯的函数或明确提供状态。

作为一个简单的例子,假设我们从示例计数器应用程序开始,我们希望每次计数器是 5 的倍数时将计数器更改为随机字体颜色。

由于随机数生成是不纯的,建议将此行为放在动作创建者中。 然而,有几种不同的动作可以改变计数器的值,增量、减量、增量Async、增量IfOdd(在这种情况下不需要修改)。

increment 和 decrement 以前不需要任何状态,因为它们以前在 reducer 中处理过,因此可以访问当前值,但是由于 reducer 不能具有或返回副作用(随机数生成),这些函数现在成为需要的不纯动作创建者了解当前计数器值以确定是否需要选择新的随机字体颜色,并且该逻辑需要在所有计数器操作创建器中复制。

显式提供当前状态的一种可能替代方法是使用 redux-thunk 并返回回调以访问当前状态。 这允许您避免修改创建操作的所有位置以提供当前值,但现在要求操作创建者知道该值在全局应用程序状态中的存储位置,这限制了在其中多次重用相同计数器的能力相同的应用程序或不同的应用程序,其中状态的结构可能不同。

上下文假设和/或依赖性降低了可重用性

再次回顾反例,您会注意到只有一个反例。 虽然页面上有许多计数器来查看/更新相同的状态是微不足道的,但如果您希望每个计数器使用不同的状态,则需要对计数器进行额外的修改。

这在如何创建通用列表作为reducer 和组件增强器之前已经讨论过

如果计数器只使用简单的动作类型,那么应用elm 架构就相对简单了。

在这种情况下,父级简单地包装动作创建者或调度器以使用任何必要的上下文来扩充消息,然后它可以直接使用本地化状态调用化简器。

虽然React Elmish 示例看起来令人印象深刻,但该示例中明显缺少两个有问题的动作创建器,incrementIfOdd 和 incrementAsync。

incrementIfOdd 依赖中间件来确定当前状态,因此需要知道它在应用程序状态中的位置。

incrementAsync 最终会直接调度一个不暴露给父组件的增量操作,因此不能用额外的上下文包装。

虽然您的提议没有直接解决这个问题,但如果 incrementAsync 被实现为一个简单的操作,将状态更改为{counter: 0, incrementAfterDelay: 1000}以触​​发商店侦听器中的副作用,那么 incrementAsync 就变成了一条简单的消息。 incrementIfOdd 是纯的,所以它既可以在减速器中实现,也可以显式提供状态......因此,如果需要,可以再次应用 elm 架构。

有副作用的动作创建者很难测试

我认为这很明显,副作用将更难测试。 一旦您的副作用以当前状态和业务逻辑为条件,它们不仅变得更加难以测试,而且变得更加重要。

您的提议使人们能够轻松编写一个测试,即状态转换将创建一个包含所需反应的状态,而无需实际执行任何反应。 反应也更容易测试,因为它们不需要任何条件状态或业务逻辑。

无法优化或批处理

John A De Goes 最近的一篇博客文章讨论了不透明数据类型(例如 IO 或 Task)用于表达效果的问题。 通过使用副作用的声明性描述而不是不透明类型,您有可能在以后优化或组合效果。

FP 的现代架构

Thunk、promise 和生成器是不透明的,因此必须使用类似于fetchPostsIfNeeded函数显式处理批处理和/或抑制重复的 api 调用等优化。

您的提议消除了fetchPostsIfNeeded并且实现reactions函数似乎完全可行,该函数可以优化多个请求和/或在请求更多或更少数据时根据需要使用不同的 api 集。

我的实现

我最近创建了一个redux

我不知道如何在不分叉 redux 的情况下做到这一点,因为有必要修改 compose 和 combineReducers 以提升对现有减速器的影响,以保持与现有减速器代码的兼容性。

但是,您的建议非常好,因为它不需要修改 redux。 此外,我认为您的解决方案在解决隐含状态问题方面做得更好,并且可能更容易组合或优化结果反应。

概括

就像 React 是“只是 ui”,并没有非常明确地说明如何实际存储或更新应用程序状态,Redux 主要是“只是存储”,并且对于如何处理副作用并没有非常明确的规定。

我从来没有因为务实和完成工作而责怪任何人,而且 redux 和中间件的许多贡献者使人们能够比以前更快更好地构建真正酷的东西。 正是由于他们的贡献,我们才走到了这一步。 特别感谢所有做出贡献的人。

Redux 很棒。 这些不是 Redux 本身的必要问题,但希望对当前架构模式以及在状态修改之后而不是之前运行效果的动机和潜在优势提出建设性的批评。

我试图理解这种方法和 redux-saga 之间的区别。 我对它隐式隐藏生成器中的状态的说法很感兴趣,因为起初,它似乎在做同样的事情。 但我想这可能取决于io.take的实现方式。 如果 saga 仅在它碰巧当前在yield处被阻止时才处理它,那么我肯定明白你的意思。 但是,如果 redux-saga 将动作排队,使得io.take将返回过去的动作,那么它似乎在做同样的事情。 无论哪种方式,您都有一些逻辑可以通过侦听动作流来异步触发dispatch动作。

不过,这是一个有趣的概念。 将 Redux 概念化为一个动作流,从中触发状态转换和效果。 在我看来,这似乎是一种替代观点,而不仅仅是将其视为状态处理器。

在事件溯源模型中,我认为这归结为 Redux 操作是“命令”(采取行动的或有请求)还是“事件”(状态的原子转换,反映在平面视图中)。 我想我们有一个足够灵活的工具,可以考虑任何一种方式。

我也有点不满意“智能动作创建者”的现状,但我一直在以不同的方式接近它,其中 Redux 更像是事件存储——其中动作是许多可能的效果之一可能由某些外部“控制器”层触发。 我将遵循这种方法的代码分解到react-redux-controller 中,尽管我对实现这一点的潜在轻量级方法有一个不成熟的想法。 然而,它需要 react-redux 有一个它目前没有的钩子,还有一些我还没有完全解决的商店包装 hijinks。

我试图理解这种方法和 redux-saga 之间的区别

直到我想出我的方法后,我才看到 redux-saga,但肯定有一些相似之处。 但我还是有些区别:

  1. 我的方法无法访问动作流,只能访问状态。 redux-saga 可以仅仅因为有一个动作就开始这个过程。 我的方法要求减速器更改触发反应函数以请求操作的状态。
  2. 我的方法要求所有状态都存在于 redux 的状态中。 Redux-saga 具有存在于 saga 生成器中的附加状态(它位于哪一行,局部变量的值)。
  3. 我的方法隔离了异步部分。 无需处理异步功能即可测试反应的实际逻辑。 传奇把这些放在一起。
  4. 传奇将相同逻辑的不同部分结合在一起。 我的方法迫使您将 saga 分成属于减速器、反应和反应类型实现的部分。

基本上,我的方法强调纯函数并将所有内容保持在 redux 状态。 redux-saga 方法强调更具表现力。 我认为有利有弊,但我更喜欢我的。 但我有偏见。

这听起来很有希望。 我认为看到一个将反应机制与领域逻辑分开的例子会更有说服力。

您的提议消除了 fetchPostsIfNeeded,并且在请求更多或更少的数据时,实现可以优化多个请求和/或根据需要使用不同的 api 集的反应功能似乎完全可行。

就目前而言,您无法在反应功能中真正做到这一点。 那里的逻辑需要知道哪些动作已经开始(我们不能再将任何东西批处理到它们中),但反应函数没有信息。 消耗反应()函数的反应机制当然可以做这些事情。

我认为看到一个将反应机制与领域逻辑分开的例子会更有说服力。

我假设您的意思是 doReactions() 函数处理 XMLHttpRequest 的启动/停止的方式? 我一直在探索不同的方法来做到这一点。 问题是很难找到一种通用的方法来检测两个反应是否实际上是同一个反应。 Lodash 的 isEqual 几乎可以工作,但无法关闭。

我假设您的意思是 doReactions() 函数处理 XMLHttpRequest 的启动/停止的方式?

不,我的意思是,在您的示例中,用于设置反应概念的所有配置都与正在获取的数据的域逻辑以及如何获取数据的详细信息混合在一起。 在我看来,通用方面应该被分解为与示例特定细节耦合较少的东西。

不,我的意思是,在您的示例中,用于设置反应概念的所有配置都与正在获取的数据的域逻辑以及如何获取数据的详细信息混合在一起。 在我看来,通用方面应该被分解为与示例特定细节耦合较少的东西。

嗯...我认为我们在域逻辑中的意思可能不同。

在我看来,reactions() 函数封装了域逻辑,并且与处理如何应用反应的逻辑的 doReactions() 函数分开。 但是你的意思好像不一样...

就目前而言,您无法在反应功能中真正做到这一点。 那里的逻辑需要知道哪些动作已经开始(我们不能再将任何东西批处理到它们中),但反应函数没有信息。 消耗反应()函数的反应机制当然可以做这些事情。

我的主要意思是,如果单个事件触发了多个组件请求相同信息的状态更改,那么它可能能够优化它们。 但是,您是对的,它本身不足以确定来自先前状态更改的副作用是否仍在等待中,因此不需要额外的请求。

我最初认为也许可以将所有状态保留在应用程序状态中,但是当我开始考虑最近的秒表问题时,我意识到虽然秒表isOn应该存储在应用程序状态中这一事实,但实际的interval与此秒表关联的isOn应该处于应用程序状态,但在这种情况下并不是单独的状态不够。

我的主要意思是,如果单个事件触发了多个组件请求相同信息的状态更改,那么它可能能够优化它们。 但是,您是对的,它本身不足以确定来自先前状态更改的副作用是否仍在等待中,因此不需要额外的请求。

我正在考虑合并或批处理请求。 消除重复应该可以正常工作。 实际上,它也应该很好地处理挂起状态更改的情况,因为在服务器响应返回之前,它们仍然会从反应函数返回(并因此去重)。

我最初认为也许可以将所有状态保留在应用程序状态中,但是当我开始考虑最近的秒表问题时,我意识到虽然秒表 isOn 应该存储在应用程序状态中,但与此秒表关联的实际间隔对象需要存放在其他地方。 isOn 应该处于应用程序状态,但在这种情况下不是单独的状态。

在我看来,当前未决的反应就像你的反应组件。 从技术上讲,它们有一些内部状态,但我们将它们建模为当前状态的函数。

嗯...我认为我们在域逻辑中的意思可能不同。

在我看来,reactions() 函数封装了域逻辑,并且与处理如何应用反应的逻辑的 doReactions() 函数分开。 但是你的意思好像不一样...

我有点把整个/reactions/index模块当作一个整体,但是,是的,我同意reactions函数纯粹是域逻辑。 但它不是在特定于域的模块中,而是与doReactions的样板包装在一起。 这并不是要打击你的方法论,它只是让你更难一目了然地理解库代码和应用程​​序代码之间的分离。

然后doReactions本身在我看来与从 API 获取数据的特定行为的特定方法相当紧密地耦合在一起。 我认为充实的反应库可能是为不同类型的效果注册处理程序的一种方式。

这不是要敲你的方法; 我觉得这种方法真的很吸引人。

我不确定反应组件状态是一个很好的类比,因为大多数反应状态
应该处于应用程序状态,但显然需要某种方式
在不能放置在调度事件中的事件之间保持状态
店铺。

我认为这种状态就是@yelouafi所说的控制状态和
也许 sagas 是一种对不可序列化状态进行建模的好方法
系统作为独立的观察者/参与者。

我想我不会担心隐藏的传奇状态,如果传奇
仅响应应用程序生成的事件(反应)而不是用户
发起的事件(动作),因为这将允许应用程序减速器使用
当前状态和任何条件业务逻辑来确定
应用程序应该允许所需的副作用而不重复
商业逻辑。
2016 年 1 月 4 日星期一下午 5:56 Winston Ewert [email protected]
写道:

我的主要意思是,如果单个事件触发了状态更改,其中
多个组件请求相同的信息,然后它可能能够
优化它们。 你是对的,但它本身并不足以
确定来自先前状态更改的副作用是否仍然未决
因此不需要额外的请求。

我正在考虑合并或批处理请求。 消除重复
应该工作得很好。 实际上,它应该处理pending state的情况
变化也很好,因为它们仍然会从
反应功能(因此去重复)直到服务器响应
回来。

我最初想也许可以将所有状态保留在应用程序中
状态,但是当我开始考虑最近的秒表问题时
意识到虽然秒表 isOn 应该存储在
应用程序状态与此关联的实际间隔对象
秒表需要存放在其他地方。 isOn 应该在应用程序中
state but is not only 在这种情况下不是充分的状态。

我的想法是,当前未决的反应就像你的
反应成分。 从技术上讲,它们有一些内部状态,但我们建模
它们作为当前状态的函数。


直接回复此邮件或在 GitHub 上查看
https://github.com/rackt/redux/issues/1182#issuecomment -168858051。

这并不是要打击你的方法论,它只是让你更难一目了然地理解库代码和应用程​​序代码之间的分离。

这是完全公平的。

然后 doReactions 本身在我看来与从 API 获取数据的特定行为的特定方法相当紧密地耦合在一起。 我认为充实的反应库可能是为不同类型的效果注册处理程序的一种方式。

是的。 我仍在尝试找出拆分它的最佳方法。 平等检查问题使情况变得复杂。

我不确定反应组件状态是一个很好的类比,因为大多数反应状态
应该处于应用程序状态,但显然需要某种方式
在不能放置在调度事件中的事件之间保持状态
店铺。

抱歉,我想我打错了比喻。 我的观点不是将外部动作状态与反应组件状态进行比较,而是与 DOM 的状态进行比较。 间隔或 XMLHttpRequest 更像是 react 创建和销毁的 DOM 元素。 你只需告诉 react 当前的 DOM 应该是什么并让它发生。 同样,您只需返回当前外部反应的集合,框架就会取消或启动动作以使其成为现实。

我觉得这种方法也很有趣。 您是否考虑过使用多个doReactions ,它们采用不同的状态映射? 我认为它类似于cyclejs,您可以在其中构建可重用的驱动程序:

function main(action$) {
  const state$ = action$.startWith(INITIAL_STATE).scan(reducer);

  return { 
    DOM: state$.map(describeDOM),
    HTTP: state$.map(describeRequests),
    ...
  };
}

一个区别是您不查询事件的驱动程序以获取操作流 ( const someEvent$ = sources.DOM.select('.class').events('click') ),而是直接指定接收器中的操作 ( <button onClick={() => dispatch(action())} /> ),就像您对 HTTP 请求所做的一样以及。

我认为 React 的比喻非常有效。 我不会认为 DOM 是内部状态,而是它所使用的 API,而内部状态由组件实例和虚拟 dom 组成。

这是 API 的一个想法(使用 React;HTTP 也可以这样构建):

// usage
const describe = (state, dispatch) => <MyComponent state={state} dispatch={dispatch} />;
const driver = createReactDOMDriver({ container } /* opts */);
store.subscribe(() => driver.update(describe(store.getState(), store.dispatch)); 
// (could be simplified further to eg. `store.use(driver, describe)` )

// implementation
const createReactDOMDriver = ({ container }) => {
  return {
    update: (element) => ReactDOM.render(element, container),
    destroy: () => ReactDOM.unmountComponentAtNode(container),
  };
};

我会让describegetState (而不是状态快照)和dispatch 。 这样,它就可以像它想要的那样异步。

您是否考虑过使用多个 doReactions,它们采用不同的状态映射?

我曾短暂地想过它,现在我要来回讨论一下。 拥有不同的反应库来做不同的事情是很自然的,一个用于 DOM,一个用于 http,一个用于计时器,一个用于网络音频等。每个人都可以根据自己的情况进行优化/行为。 但是,如果您有一个执行一堆一次性外部操作的应用程序,那么它似乎不太有用。

我会让描述采取 getState(而不是状态快照)和调度。 这样,它就可以像它想要的那样异步。

我不会。 在我看来,我们希望尽可能限制异步,而不是提供额外的使用方法。 您可能想要调用 getState() 的任何内容都应该在减速器或反应函数中完成。 (但那是我纯粹的心态,也许不遵循它有一个务实的理由。)

有道理。 我还没有完全考虑你的想法和@taurose的例子之间的映射。 我匆忙假设describereactions函数,但这可能不是真的。

但是,是的,我同意限制异步是理想的,因为如果我理解您的想法的主旨,我们希望延续是纯粹的,并且将 1:1 映射到状态中的特定方面,例如存在描述意图的数组成员特定的效果正在进行中。 这样,它们是否被多次执行并不重要,并且没有隐藏的进程在其他进程可能隐式依赖的某个流程中的某个地方停滞。

我会让描述采取 getState(而不是状态快照)和调度。 这样,它就可以像它想要的那样异步。

describe在每次状态更改时都会被调用,所以我认为没有必要这样做。 这并不意味着它不能进行异步。 考虑反应组件:您不会在渲染方法或事件处理程序中调用getState来获取当前状态,而是从 props 中读取它。

但是你是对的,它不能(不应该)自己做任何异步的事情; 它应该把它留给驱动程序,只需将一些映射状态和/或回调传递给它。

假设 describe 是反应函数,但这可能不是真的。

据我所知,这几乎是一样的。 一个区别是reactions没有得到dispatch 。 因此,虽然describe返回创建和调度动作的回调,但reactions返回动作创建者。

@winstonewert这是一个很长的@yelouafi可以回答您。

redux-saga 项目起源于这里的长期讨论

我还在一个生产应用程序上使用了一年多的 saga 概念,实现的表现力较差,但不是基于生成器。 以下是我给出的关于 redux 概念的一些伪示例:

这里的实现远非完美,但它只是提供了一个想法。

@yelouafi意识到使用将状态隐藏在 redux 之外的生成器固有的

redux-saga 之于 redux-thunk 就像 Free 之于 IO monad 一样。 效果是声明性的,现在不执行,可以内省并在解释器中运行(您可以在将来自定义)

我理解你关于生成器内部隐藏状态的观点。 但实际上 Redux 商店是 Redux 应用程序的真实来源吗? 我不这么认为。 Redux 记录动作,并重放它们。 您可以随时重播这些操作以重新创建商店。 redux 存储就像事件日志的 CQRS 查询视图。 这并不意味着它必须是该事件日志的唯一一个投影。 您可以在不同的查询视图中投影相同的事件日志,并在 sagas 中监听它们,无论使用何种技术,sagas 都可以使用生成器、全局可变对象或 reducer 管理它们的状态。

Imho用reducer创建saga概念在概念上并不是一个坏主意,我同意你的看法,这是一个权衡决定。
就个人而言,在生产中使用 saga 一年多之后,我不记得有任何用例可以对 saga 的状态进行快照并稍后恢复它,所以我更喜欢生成器的表现力,即使我失去了它特征。

我希望我所说的一切都不会被认为是对 redux-saga 的攻击。 我只是在谈论它与我提出的方法有何不同。

我理解你关于生成器内部隐藏状态的观点。 但实际上 Redux 商店是 Redux 应用程序的真实来源吗? 我不这么认为。 Redux 记录动作,并重放它们。 您可以随时重播这些操作以重新创建商店。 redux 存储就像事件日志的 CQRS 查询视图。 这并不意味着它必须是该事件日志的唯一一个投影。 您可以在不同的查询视图中投影相同的事件日志,并在 sagas 中监听它们,无论使用何种技术,sagas 都可以使用生成器、全局可变对象或 reducer 管理它们的状态。

我真的不明白你在这里的意思。 您似乎在争辩说传奇是事件日志的投影? 但事实并非如此。 如果我重播这些动作,如果传奇依赖于异步事件,我将不会到达传奇中的同一个地方。 在我看来,传奇产生的状态既不在 redux 的状态存储中,也不在事件日志的投影中,这似乎是不可避免的。

据我所知,这几乎是一样的。 一个区别是反应不会得到调度。 因此,虽然描述返回创建和分派动作的回调,但反应返回动作创建者。

同意。 原则上,react 可以使用相同的接口,所有事件处理程序都将采用一个动作创建者,该动作创建者将在事件触发时被分派。

我想得越多,我认为这种方法和传奇之间可能会有很多协同作用。 我完全同意@winstonewert 概述的四点。 我认为反应看不到用户发起的动作是一件好事,因为这可以防止隐藏状态并确保减速器中的业务逻辑不需要在动作创建者或传奇中重复。 但是,我意识到副作用通常会创建不可序列化的状态,无法存储在反应存储、间隔、dom 对象、http 请求等中。 sagas、rxjs、baconjs 等非常适合这种外部不可序列化的控制状态。

doReactions 可以用 saga 替换,saga 的事件源应该是反应而不是动作。

我希望我所说的一切都不会被认为是对 redux-saga 的攻击

一点也不。 我一直在关注讨论,但不想在不仔细查看您的代码的情况下发表评论。

乍一看。 看来你只对状态变化做出反应。 正如我所说,这是快速浏览。 但它似乎会使实现复杂的流程比 elm 方法(您同时采取状态和行动)更加困难。 这意味着您必须将更多控制状态存储到商店中(仅应用状态变化不足以推断相关反应)

当然,没有什么能打败纯函数。 我认为 reducer 非常适合表达状态转换,但是当你将它们变成状态机时会变得非常奇怪。

这意味着您必须将更多控制状态存储到商店中(仅应用状态变化不足以推断相关反应)

是的。 在我看来,这是这种方法的关键差异化方面。 但我想知道这个问题是否可以透明化,在实践中,不同的效果类型是否可以包含在不同的“驱动程序”中? 我想人们很容易选择他们想要的驱动程序或编写自己的驱动程序以获得新颖的效果。

但是,我意识到副作用通常会创建不可序列化的状态,无法存储在反应存储、间隔、dom 对象、http 请求等中。 sagas、rxjs、baconjs 等非常适合这种外部不可序列化的控制状态。

我还没有看到你是什么。

我认为 reducer 非常适合表达状态转换,但是当你将它们变成状态机时会变得非常奇怪。

我同意。 如果您正在手写一个复杂的状态机,我们就会遇到问题。 (实际上,如果我们可以将生成器转换为减速器就好了)。

但我想知道这个问题是否可以透明化,在实践中,不同的效果类型是否可以包含在不同的“驱动程序”中? 我想人们很容易选择他们想要的驱动程序或编写自己的驱动程序以获得新颖的效果。

我不确定你在这里想什么。 我可以看到不同的驱动程序在做不同的有用的事情,但是消除了控制状态?

@winstonewert不,我不会将任何事情视为攻击。 我什至没有时间真正查看您的代码:)

我真的不明白你在这里的意思。 您似乎在争辩说传奇是事件日志的投影? 但事实并非如此。 如果我重播这些动作,如果传奇依赖于异步事件,我将不会到达传奇中的同一个地方。 在我看来,传奇产生的状态既不在 redux 的状态存储中,也不在事件日志的投影中,这似乎是不可避免的。

不,我不是,redux store 是一个投影,但 saga 是一个普通的简单监听器。

Saga(也称为流程管理器)并不是一个新概念,它起源于 CQRS 世界,过去曾广泛用于后端系统。

传奇不是事件日志到数据结构的投影,它是一个编排,可以监听系统中发生的事情并发出反应,其余的是实现细节。 通常,传奇正在侦听事件日志(可能还有其他外部事物,例如时间……)并且可能会产生新的命令/事件。 此外,当您在后端系统中重放事件时,您通常会禁用禁用由传奇触发的副作用。

不同之处在于,在后端系统中,saga 通常实际上是事件日志的投影:要更改其状态,它必须发出事件并自行侦听它们。 在当前实现的 redux-saga 中,重放事件日志以恢复 saga 状态会更困难。

我不确定你在这里想什么。 我可以看到不同的驱动程序在做不同的有用的事情,但是消除了控制状态?

不,不是消除它,只是使它成为大多数目的的底层实现问题。

在我看来,在 Redux 社区中有非常强烈的共识,即在商店中存储域状态是一个巨大的胜利(否则,你为什么要使用 Redux?)。 与将 UI 状态封装在组件中相比,存储 UI 状态是一种胜利的共识要少一些。 然后是在商店中同步浏览器状态的想法,例如 URL(redux-simple-router)或表单数据。 但这似乎是在商店中存储长期运行过程的状态/阶段的最后边界。

抱歉,如果这是切线,但我认为具有良好开发人员可用性的高度通用方法必须具有以下功能:

  • 使典型用户不必真正关心效果在商店中的表现形式。 他们应该与简单的 API 交互,这些 API 抽象了这些细节。
  • 使效果易于组合。 做诸如控制流和视其他效果而定的效果之类的事情应该感觉很自然。 当然,这是生成器抽象真正闪耀的地方。 它适用于大多数控制流,闭包是一个明显的例外。 但是很容易看出在 redux-saga 或 react-redux-controller 中可以表达多么复杂的异步流。
  • 使效果状态可以在需要时轻松地显示给其他商店消费者。 这将允许您执行诸如向用户呈现多效过程的状态之类的操作。
  • 也许这是显而易见的,但是任何封装状态的子系统都会通过分派动作来与 Redux 同步该状态。

对于第二点,我认为必须有一些与 redux-saga 非常相似的东西。 它的call包装器可能与我的想法非常接近。 但是从某种意义上说,saga 必须是“可快速转发的”,才能让您将其反序列化为中间状态。

这是一项艰巨的任务,但实际上,我认为如果拥有一个中央的、可序列化的动作记录,在非常细粒度的级别跟踪整个应用程序的状态,会取得重大胜利,这将是利用它的方法。 而且我认为那里可能确实有很大的胜利。 我正在想象一种更简单的方法来使用用户和性能分析来检测应用程序。 我想象着非常惊人的可测试性,其中不同的子系统仅通过状态耦合。

我现在可能已经偏离了方向,所以我要离开了:)

@acjay我认为我们同意你的观点,问题是找到正确解决所有这些问题的实现:)

但是似乎很难同时拥有一个带有生成器的富有表现力的 api,以及时间旅行和快照/恢复状态的可能性......也许可以记住效果的执行,以便我们可以轻松地恢复生成器状态......

不确定,但这可能会排除while(true) { ... }风格的传奇。 循环只是状态进程的结果吗?

@acjay @slorber

正如我在 (https://github.com/yelouafi/redux-saga/issues/22#issuecomment-168872101) 中解释的那样,saga 可以单独进行时间旅行(即没有热重载)。 将传奇带到特定点所需的只是从开始到该点产生的一系列效果,以及它们的结果(解决或拒绝)。 然后你就用那个序列来驱动发电机

在实际的 master 分支中(尚未在 npm 上发布)。 Sagas 支持监控,他们将所有产生的效果,以及他们的结果作为行动发送到商店; 它们还提供层次结构信息来跟踪控制流图。

可以利用该效果日志重播 Saga 直到给定点:无需进行真正的 api 调用,因为日志已经包含过去的响应。

在 repo 示例中,有一个 saga 监视器示例(作为 Redux 中间件实现)。 它侦听效果日志并维护内部树结构(构建良好的懒惰)。 您可以通过将操作{type: 'LOG_EFFECT'}分派到商店来打印流的跟踪

这是异步示例中效果日志的捕获

saga-log-async

编辑:抱歉,固定图片链接

耐人寻味! 那个开发工具图像是_awesome_。

这很酷 :)

确实,那个传奇监视器非常酷。

仔细想想,在我看来,saga 正在解决两个问题。 首先,它处理异步效果。 其次,它处理复杂的状态交互,否则需要在减速器中使用令人讨厌的手写状态机。

我的方法只解决第一个问题。 我没有发现第二个问题的需要。 可能我还没有编写足够的 redux 代码来运行它。

是的,但我想知道是否有办法融合这两个想法。 redux-saga 的call包装器是一个非常简单的效果间接层,但是假设您可以使用不同类型效果的驱动程序初始化中间件,您可以将这些效果表示为 JSONable 数据,与函数分离这实际上是被调用的。 驱动程序将处理将底层状态更改分派到存储的细节。

这可能会增加很多复杂性,但实际上却没有什么好处。 但只是试图按照这种思路进行到底。

好的,我已经把更多的库放在一起,并移植了真实世界的例子来使用它:

首先,我们有反应的实现:
https://github.com/winstonewert/redux-reactions/blob/master/src/index.js
该接口由三个函数组成:startReactions 接受存储、一个反应函数以及从名称到驱动程序的映射。 fromEmitter 和 fromPromiseFactory 都创建驱动程序。

这里的示例调用 startReactions 来启用系统:
https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/store/configureStore.dev.js#L28

反应的基本配置在这里:
https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/index.js。
反应函数实际上只是遍历反应路由器实例化的组件,寻找具有反应()函数的组件,以确定该页面实际需要的反应。

github api 反应类型的实现在这里: https : https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/api.js#L79 ,它使用 fromPromiseFactory 从一个函数创建驱动程序回报承诺。

在此处查看组件特定的反应函数: https :

反应创建者和通用逻辑在https://github.com/winstonewert/redux-reactions/blob/master/examples/real-world/reactions/data.js

嗨伙计! Raise 刚刚发布了一个商店增强器,让您也可以使用类似 Elm 架构的效果系统! 我希望我们能够学习和改进所有这些方法,以满足社区的所有需求 :smile:

https://github.com/raisemarketplace/redux-loop

任何对讨论感兴趣的人都可以在这里看到关于我的想法的进一步讨论: https :

你也可以在这里查看一个分支,在那里我使用我的模式重新设计计数器应用程序以使其更加优雅:
https://github.com/winstonewert/redux-reactions/tree/elmish/examples/counter

我还发现我正在重新发明这里使用的方法: https :

@yelouafi ,你能重新发布传奇监视器想法的链接吗? 这是一些非常棒的东西! 该链接似乎已失效(404)。 我很想看到更多!

相关新讨论: https :

(我相信这是相关的。对不起,如果那是错误的地方)

我们是否可以将所有效果都视为 DOM 渲染?

  1. jQuery是一个带有命令式接口的 DOM 驱动程序。 React 是一个具有声明式接口的 DOM 驱动程序。 因此,不是命令:“禁用那个按钮”,而是声明:“我们需要禁用那个按钮”,并且驱动程序决定要执行哪些 DOM 操作。 而不是订购:“ GET \product\123 ”,我们声明:“我们需要该数据”,驱动程序决定发送/取消哪些请求。
  2. 我们使用 React 组件作为 DOM 驱动程序的 API。 让我们也使用它们来连接其他驱动程序。

    • <button ...> - 我们基于“普通” React 组件构建我们的 View 层

    • <Map ...> - 我们使用“包装器”组件将某些库的命令式接口转换为声明式接口。 我们以与“普通”组件相同的方式使用它们,但在内部它们实际上是驱动程序。

    • <Chart ...> - 根据实现的不同,这可以是上述任何一种。 因此,“正常”组件和驱动程序之间的界限已经模糊。

    • <Http url={'/product/'+props.selectedProductId} onSuccess={props.PRODUCT_LOADED} /> (或“智能” <Service...> )——我们在(无UI)驱动程序组件上构建我们的服务层

View 和 Service 层都是通过 React 组件描述的。 我们的顶级(连接)组件将它们粘合在一起。
这样我们的 reducer 就保持纯粹,我们不会引入任何新的方法来处理效果。

不确定new DateMath.random在这里如何适合。

是否总是可以将命令式 API 转换为声明式 API?
你认为这是一个可行的观点吗?

谢谢

鉴于我们有用于异步操作的传奇和其他很棒的工具,我认为我们现在可以安全地关闭它。 查看 #1528 以了解一些有趣的新方向(除了异步之外)。

此页面是否有帮助?
0 / 5 - 0 等级