异步生成器_异步生成器作为状态管理的替代方法

异步生成器

Async Generators is a simple but powerful feature that is now a part of JavaScript. It unlocks many new tools to improve software architecture, making it more agile, simplifying extension and composition.

异步生成器是一个简单但功能强大的功能,现在已成为JavaScript的一部分。 它解锁了许多新工具来改善软件体系结构,使其更加敏捷,简化了扩展和组成。

TL; DR (TL;DR)
  • With Async Generators there is no longer need for components state, state management tools, component lifecycle methods, and even the latest React Context, Hooks and Suspense APIs. It is much simpler to develop, maintain, and test.

    使用异步生成器,不再需要组件状态,状态管理工具,组件生命周期方法,甚至不需要最新的React Context,Hooks和Suspense API。 开发,维护和测试要简单得多。
  • Unlike a state management approach, async generators tame asynchronicity leaving mutations harmless (if visible only in the generator’s scope).

    与状态管理方法不同,异步生成器会驯服异步性,从而使突变无害(如果仅在生成器的范围内可见)。
  • This approach has a functional programming background.

    这种方法具有功能编程背景。
  • State persistence for things like time traveling, universal apps is also available.

    还提供状态持久性,例如时间旅行,通用应用程序。
  • The article uses React and JavaScript, but the technique is applicable in any other framework or programming language with generators (coroutines).

    本文使用React和JavaScript,但该技术适用于带有生成器(协程)的任何其他框架或编程语言。
  • I’m advertising my tool only at the end and very briefly. Most of the article is about async generators without any dependency.

    我只是在最后简短地宣传我的工具。 本文大部分内容是关于异步生成器的,没有任何依赖性。

Let’s start with a statement from Redux motivation page:

让我们从Redux激励页面的声明开始:

This complexity is difficult to handle as we’re mixing two concepts that are very hard for the human mind to reason about: mutation and asynchronicity. I call them Mentos and Coke. Both can be great in separation, but together they create a mess.

这种复杂性很难处理,因为我们混用了两个人类大脑难以理解的概念突变和异步性。 我称他们为Mentos和Coke 两者之间的分隔可能很棒,但在一起却会造成混乱。

Redux and other state management tools are mostly focusing on restricting or controlling data mutations. Async generators can handle asynchronicity. This makes mutation safe if it is visible only within a particular generator scope.

Redux和其他状态管理工具主要致力于限制或控制数据突变。 异步生成器可以处理异步性。 如果仅在特定生成器范围内可见,则使突变安全。

All the common state management techniques can be split into two big classes.

所有常见的状态管理技术都可以分为两大类。

The first class maintaining data dependencies graph to propagate changes through handlers — React Component State, MobX, RxJS. Maintaining these dependencies is a complex task. The underlying libraries are taking charge of part of this complexity by managing subscriptions, optimizing the order of handlers execution, batching them, but it is still confusing to use sometimes, often requires hard fine-tuning, e.g., with shouldComponentUpdatemethod.

第一类维护数据依赖关系图以通过处理程序传播更改-React Component State,MobX,RxJS。 维护这些依赖关系是一项复杂的任务。 基础库通过管理订阅,优化处理程序的执行顺序,批处理它们来负责这种复杂性的一部分,但有时仍然shouldComponentUpdate ,常常需要进行微调,例如使用shouldComponentUpdate方法。

Another approach limits mutation to only a single cell (storage) (e.g., Redux). This needs much smaller libraries, with less magic in them. It is more a pattern than a library. Unfortunately, the programs are more verbose, and this breaks data encapsulation. There are many patterns, wrappers to solve this though, but they make a single cell approach to be more similar to the graph based one.

另一种方法将突变限制为仅单个单元格(存储)(例如Redux)。 这需要更小的库,并且其中包含的魔术更少。 它比库更像是一种模式。 不幸的是,程序更加冗长,这破坏了数据封装。 虽然有很多模式,但包装器可以解决此问题,但是它们使单单元格方法与基于图的方法更加相似。

The technique in this story and Redux are both based on Event Sourcing pattern, and they have many similarities. It also offers encapsulated data and synchronous deterministic order of executions for operations with side effects.

这个故事中的技术和Redux都是基于事件源模式,并且它们有很多相似之处。 它还为具有副作用的操作提供了封装的数据和执行的同步确定性顺序。

This approach can be abstractly viewed as a dependency graph as well, but the changes are propagated in reverse direction, from its root to towards leaves of its spanning tree. In each node we check should the propagation proceed to children or not. This makes the scheduling algorithm very lightweight and easy to control. It doesn’t require any library, basing only on JavaScript built-in features.

这种方法也可以抽象地视为依赖图,但更改是从其根到其生成树的叶以相反方向传播的。 在每个节点中,我们检查传播是否继续向子节点传播。 这使得调度算法非常轻巧并且易于控制。 它仅基于JavaScript内置功能就不需要任何库。

Let’s first port Redux VanillaJS counters example to illustrate the idea.

让我们首先移植Redux VanillaJS计数器示例来说明这个想法。

The original reducer is replaced with async generator function. The function calculates and stores its state in a local variable. It also yields the calculated value, the new value is stored in the singleton storage, and it is visible from event handlers. I’ll remove that singleton storage in the next steps.

原始的reducer被异步生成器功能取代。 该函数计算其状态并将其存储在局部变量中。 它还会产生计算出的值,新值存储在单例存储中,并且从事件处理程序中可见。 我将在后续步骤中删除该单例存储。

This version doesn’t look much different from Redux. The async generator there could be Redux storage middleware. This violates one of Redux principles though, namely storing all application state only in the storage. Even if the generator doesn’t have any local variables, it still has its execution state — the position in the code where the execution is suspended in yield or await.

这个版本看起来与Redux没有什么不同。 那里的异步生成器可能是Redux存储中间件。 但是,这违反了Redux 原则之一,即仅将所有应用程序状态存储在存储中。 即使生成器没有任何局部变量,它仍然具有其执行状态-代码在代码中的位置,在该位置执行以yieldawait挂起。

内外翻转组件 (Turning components inside-out)

Generator functions are functions returning iterators. We can do with them everything we can do with plain functions. For example, by composing generator functions, we can split computation into a few independent stages.Each stage has own encapsulated state. Each stage receives messages which were yielded on the previous stage, handles them yielding another messaging and passing them to the next stage.

生成器函数是返回迭代器的函数。 我们可以使用简单功能来处理它们。 例如,通过组合生成器函数,我们可以将计算分为几个独立的阶段,每个阶段都有自己的封装状态。 每个阶段都接收在上一阶段产生的消息,对其进行处理以产生另一条消息并将它们传递到下一个阶段。

The payload of the messages can contain VDOM elements. Instead of having a monolithic components tree we emit parts of it and send them to the next stage, where they can be assembled or transformed. Here is the same Counters example with React.

消息的有效负载可以包含VDOM元素。 我们不再使用整体组件树,而是释放其中的一部分,然后将它们发送到下一个阶段,在这里可以对它们进行组装或变形。 这是与React相同的Counters示例。

There pipe function is a function composition. The functions take two arguments. The first is async iterable for messages from the former stage. And the second is to send a message into the start of the pipe. It should be called only from event handlers. This function can be replaced soon with JavaScript embedded pipeline operator.

pipe功能是一个功能组成。 这些函数有两个参数。 第一个是异步迭代的,用于来自前一阶段的消息。 第二个是将消息发送到管道的开头。 应该仅从事件处理程序中调用它。 此功能可以很快用JavaScript嵌入式管道运算符替换。

When we compose plain functions the next one in chain starts executing only after the previous was finished. While for generators (and in fact any coroutines) the execution can be suspended in interleaved with other functions. This makes composing different parts easier.

当我们编写普通函数时,链中的下一个仅在前一个函数完成后才开始执行。 对于生成器(实际上是任何协程),可以将其执行与其他功能交错起来中止。 这使组成不同的部分变得更加容易。

The example above briefly shows extensibility by decoupling a few menu buttons from the root component into a separate stage. Instead of abstracting menu buttons into a separate component it maintains a placeholder where it injects components it receives in messages with “MENU_ITEM” type. It is an Inversion of Control for components. Both techniques React Components and these Inverted Components can be used together of course.

上面的示例通过将几个菜单按钮与根组件分离到一个单独的阶段中来简要展示了可扩展性。 与其将菜单按钮抽象为一个单独的组件,不如保留一个占位符,将占位符注入到接收到的“ MENU_ITEM”类型的消息中。 这是组件的控制反转。 当然,这两种技术React组件和这些反向组件都可以一起使用。

延期 (Extension)

An exciting point of this technique is nothing should be preliminary designed to make the program reusable and decoupled. Nowadays premature abstraction is probably a bigger evil than premature optimization. It almost definitely leads to an overdesigned mess impossible to use. Using abstract generators, it is easy to keep calm and implement the required features, splitting when needed, without thinking about future extensions, easy to refactor or abstract some common parts after more details are available.

这项技术的令人兴奋的一点是,应进行初步设计以使程序可重用和解耦。 如今,过早抽象可能比过早优化更大。 几乎肯定会导致无法使用的过度设计的混乱。 使用抽象生成器,很容易保持镇定并实现所需的功能,在需要时进行拆分,而无需考虑将来的扩展,在获得更多详细信息后易于重构或抽象一些常用部分。

Redux is famous for making programs simpler to extend and reuse. The approach in this story is also based on Event Sourcing, but it is much simpler to run async operations and it doesn’t have a single store bottleneck, nothing should be designed prematurely.

Redux以使程序更易于扩展和重用而闻名。 这个故事中的方法也是基于事件源的,但是运行异步操作要简单得多,并且它没有单个存储瓶颈,因此不应过早设计任何东西。

Many developers like single storage because it is easy to control. The control is not a free thing though. One of the widely accepted advantages of Event Sourcing pattern is an absence of a central DB. It is simpler to change one part without danger of breaking something else. There is another problem of single storage discussed in Persistence section below.

许多开发人员喜欢单一存储,因为它易于控制。 控件不是免费的东西。 Event Sourcing模式的广泛接受的优势之一是没有中央数据库。 更改一个零件而没有破坏其他零件的危险比较简单。 在下面的“持久性”部分中讨论了单个存储的另一个问题。

There is Decouple Business Logic article with more detailed cases study. At some step there, I added a multi-select feature to drag and drop without changing anything in single element handling. With a single store, this would mean changing its model from storing a single currently dragging element to a list.

Decouple Business Logic文章,其中有更详细的案例研究。 在该处的某个步骤中,我添加了一个多选功能,可以拖放而无需更改单个元素处理中的任何内容。 对于单个存储,这意味着将其模型从存储单个当前拖动的元素更改为列表。

There are similar solutions in Redux, namely applying higher order reducer. It could take a reducer working with a single element and translate into reducer working for a list. The generators solution uses higher order async generators instead, taking a function for a single element and generating the one for a list. It is similar but much less verbose, as the generator encapsulates data and implicit control state.

Redux中有类似的解决方案,即应用高阶减速器。 可能需要一个化简处理单个元素,然后转化为化简处理列表。 生成器解决方案改用高阶异步生成器,为单个元素采用一个函数,然后为列表生成一个。 它与生成器封装数据和隐式控制状态类似,但更不那么冗长。

As an illustration, let’s make a list of counters. This step is covered in “Decouple Business Logic” article, I’m not giving many details here. The forkfunction is the async iterators transforming function, running its argument in threads per item. It is a not simple, but it is a generic one, works in many contexts as is. In the next section, for example, I apply it recursively to get a tree view.

作为说明,让我们列出一个计数器列表。 “解耦业务逻辑”一文中介绍了此步骤,在此不赘述。 fork函数是异步迭代器转换函数,在每个项目的线程中运行其参数。 这不是一个简单的操作,但是它是一个通用的操作,可以在许多情况下照常工作。 例如,在下一节中,我将其递归应用以获取树视图。

性能 (Performance)

Async generators overhead is much smaller than for state management libraries. But there are a lot of ways to get performance problems here too, e.g. over flooding with messages. But there are also a lot of almost effortless ways to improve performance.

异步生成器的开销比状态管理库的开销小得多。 但是,这里也有很多方法可以解决性能问题,例如消息泛滥。 但是,还有许多几乎不费吹灰之力即可提高性能的方法。

In the former example, there are useless calls to ReactDom.render. This is obviously a performance problem, and there is a simple solution. Solving it quickly by sending another message with type “FLUSH” after each dispatched event. React render runs only after it receives this message. The intermediate steps can yield whatever they need in between.

在前面的示例中,没有对ReactDom.render调用。 这显然是一个性能问题,并且有一个简单的解决方案。 通过在每个分派事件之后发送另一种类型为“ FLUSH”的消息来快速解决该问题。 React render仅在收到此消息后运行。 中间步骤可以产生它们之间所需的任何东西。

Another awesome side of this approach is you may not worry about performance until it is a problem. Everything is structured in small autonomous stages. They are easy to refactor, or even without refactoring — many performance problems can be solved by adding another generic state in the pipe of steps, e.g., batching, prioritizing, saving intermediate data, etc.

这种方法的另一个很棒的方面是,您可能不必担心性能,直到出现问题为止。 一切都由小型自主阶段构成。 它们很容易重构,甚至不需要重构-许多性能问题可以通过在一系列步骤中添加另一个通用状态来解决,例如批处理,确定优先级,保存中间数据等。

For example, in the demo constructed React elements are saved in local variables and React can re-use them. Changes are propagated from the root towards leaves, so optimizations like overridingshouldComponentUpdate aren’t needed.

例如,在演示中,构造的React元素保存在局部变量中,React可以重新使用它们。 更改从根部传播到叶子,因此不需要诸如覆盖shouldComponentUpdate类的优化。

测试中 (Testing)

Comparing to Redux reducer tests, generators fit a bit darker box testing strategy. The tests don’t have access to the current state. Though still, they are very simple to write. With Jest snapshots, the test can be a list of input messages with comparing output using snapshots.

与Redux减速器测试相比,生成器适合暗盒测试策略。 测试无权访问当前状态。 虽然如此,但它们的编写非常简单。 使用Jest快照,测试可以是输入消息的列表,并使用快照比较输出。

test("counterControl", async () => {
  expect.assertions(3)
  for await(const i of Counter.mainControl([
         {type:"MENU", value:<span>Menu</span>},
         {type:"VALUE", value:10},
         {type:"CONTROL", value:<span>Control</span>},
         {type:"FLUSH"},
         {type:"VALUE", value: 11},
         {type:"FLUSH"}]))
    if (i.type === "CONTROL")
      expect(renderer.create(i.value).toJSON()).toMatchSnapshot()
})

If you prefer unit-tests as documentation policy, there are many ways to make a self-documenting API for testing. Say, a function `eventually`/`until` as an addition to traditional BDD expressions.

如果您更喜欢将单元测试作为文档策略,则有很多方法可以创建用于测试的自文档API。 说,函数“最终” /“直到”是传统BDD表达式的补充。

持续状态 (Persistent state)

There is another motivation for Redux described in You Might Not Need Redux article by Dan Abramov — namely providing access to the state and it can be serialized, cloned, diffed, patched, etc. This can be used for time travel, hot reloading, universal applications and more.

Dan Abramov在“ 您可能不需要Redux”一文中描述了Redux的另一种动机-即提供对状态的访问,并且可以对其进行序列化,克隆,差异,修补等。这可以用于时间旅行,热重载,通用应用程序等等。

For this to work, the whole application state should be kept in Redux storage. Many Redux applications(even Redux samples) have some part of state stored outside of their store. These are components state, closures, generators or async functions state. Redux based tools can not persist this state.

为此,应将整个应用程序状态保存在Redux存储中。 许多Redux应用程序(甚至Redux样本)的某些状态部分都存储在其存储区之外。 这些是组件状态,闭包,生成器或异步函数状态。 基于Redux的工具无法保持此状态。

Having a single source of truth as a single storage Redux, of course, makes programs simpler. Unfortunately, it is often impossible. Consider for example distributed application, e.g., data are shared between frontend and backend.

当然,只有一个事实来源作为单个存储Redux会使程序更简单。 不幸的是,这通常是不可能的。 考虑例如分布式应用,例如,数据在前端和后端之间共享。

Event Sourcing is very successful for distributed applications. With generators, we can write a proxy sending all incoming messages to the remote side and yielding all received messages. There can be separate pipelines on each peer, or it can be the same application but a few running processes. Many configurations are easy to set up, use and re-use.

事件源对于分布式应用程序非常成功。 使用生成器,我们可以编写一个代理,将所有传入消息发送到远程端并产生所有接收到的消息。 每个对等方上可以有单独的管道,也可以是相同的应用程序,但有几个正在运行的进程。 许多配置易于设置,使用和重复使用。

For example pipe(task1, remoteTask2, task3). Here remoteTask2may be either proxy or, it may be defined here, say, for debugging purposes.

例如pipe(task1, remoteTask2, task3) 。 此处remoteTask2可以是代理,也可以在此处定义,例如用于调试。

Each part maintains its own state, it doesn’t need to be persistent. Say if each task is implemented by a separate team they are free to use any model for the state, change it any time without worrying about the other team’s work is broken.

每个部分都保持自己的状态,不需要保持持久性。 假设每个任务是由一个单独的团队执行的,他们可以自由地使用状态的任何模型,可以随时更改它,而不必担心其他团队的工作被破坏了。

This fits well for Server Side Rendering too. Say, there can be a particular higher order function to cache resulting values depending on inputs on the back-end.

这也非常适合服务器端渲染。 说,可以有一个特定的高阶函数来缓存结果,具体取决于后端的输入。

const backend = pipe(
    commonTask1,    
    memo(pipe(         
        renderTask1,         
        renderTask2)),
    commonTask2)

Here the memo higher order function examines incoming messages and may find out some calculation may be reused. This may be a server-side rendered string, and some next stage builds HTTP response with it.

在这里, memo高阶函数检查传入的消息,并可能发现某些计算可以重用。 这可能是服务器端呈现的字符串,并且下一阶段将使用它来构建HTTP响应。

The render tasks can run async operations, requesting something remote. For better user experience we want pages to load fast. To increase initial page load time applications can load components lazily displaying some loading placeholder instead of the component until it is ready. Having a few such components on a page with a bit different loading time causes page re-layouts worsening user experience.

渲染任务可以运行异步操作,请求远程操作。 为了获得更好的用户体验,我们希望页面能够快速加载。 为了增加初始页面加载时间,应用程序可以延迟显示一些加载占位符而不是组件来加载组件,直到准备就绪为止。 页面上的几个此类组件的加载时间略有不同,会导致页面重新布局,从而恶化用户体验。

React team recently announced Suspense API to solve this problem. It is an extension of React embedded into its renderer. With the Inverted Components like in this article, Suspense API isn’t needed, the solution is much simpler and not a part of UI framework.

React团队最近宣布了Suspense API来解决这个问题。 它是React嵌入到其渲染器中的扩展。 对于像本文这样的Inverted Components,不需要Suspense API,该解决方案更加简单,并且不是UI框架的一部分。

Say the application uses dynamic imports to load lazy controls, this can be done with:

假设应用程序使用动态导入来加载惰性控件,可以通过以下方式完成:

yield {type:”LAZY_CONTROL”}
yield {type:”CONTROL”, value: await import(“./lazy_component”)}

There is another generic next stage. It collects all “LAZY_CONTROL” messages, and awaits either all “CONTROL” messages are received after or a threshold time interval. After, it emits “CONTROL” messages either with the loaded control or with loading indicator placeholder. All the next updates can be batched as well using some specific timeout to minimize re-layouts.

还有另一个通用的下一阶段。 它收集所有“ LAZY_CONTROL”消息,并等待所有“ CONTROL”消息在阈值时间间隔之后或阈值时间间隔被接收。 之后,它通过加载的控件或加载指示器占位符发出“ CONTROL”消息。 还可以使用某些特定的超时来批处理所有后续更新,以最大程度地减少重新布局。

Some generator can also reorder messages to give animation a bigger priority than server data updates. I’m not even sure there are needs for a server-side framework. A tiny generator could transform initial HTTP request into messages or threads depending on URL, auth session, etc.

某些生成器还可以对消息进行重新排序,以赋予动画比服务器数据更新更大的优先级。 我什至不确定是否需要服务器端框架。 微型生成器可以根据URL,身份验证会话等将初始HTTP请求转换为消息或线程。

功能编程 (Functional Programming)

Commonly used state management tools have FP background. The code from the article doesn’t look like FP in JavaScript because of imperative for-of/switch/breakstatements. It has a corresponding concept in FP too. It is so called Monads do-notation. For example one of their use in Haskell is to resolve problems like React components property drilling.

常用的状态管理工具具有FP背景。 由于强制性的for-of/switch/break语句,本文中的代码与JavaScript中的FP看起来不一样。 FP中也有相应的概念。 这就是所谓的Monads do-notation。 例如,它们在Haskell中的用途之一就是解决诸如React组件属性钻探之类的问题。

To keep this story practical I don’t digress from the main subject here, there is another article — Using Generators as syntax sugar for side effects.

为了使这个故事切实可行,在这里我不偏离主要主题,还有另一篇文章- 使用Generators作为副作用的语法糖

Effect.js (Effectful.js)

Effectful.js is a babel preset implementing do-notation working for any monad without any JavaScript syntax extension. It also supports state persistence with a reference implementation in es-persist library. For example, this may be used to convert all async generators example above into pure functions.

Effectful.js是一个babel预设,实现了在没有任何JavaScript语法扩展的情况下对任何monad起作用的注解符号。 它还通过es-persist库中的参考实现来支持状态持久性。 例如,这可以用于将上述所有异步生成器示例转换为纯函数。

State persistence is not the primary goal of the tool. It is for higher level business logic description. Nevertheless, the tool is abstract and has many purposes. I’ll write about them more soon.

状态持久性不是该工具的主要目标。 用于更高级别的业务逻辑描述。 但是,该工具是抽象的,具有许多用途。 我会尽快写关于它们的文章。

Here is the summary sample on GitHub with all the feature above plus automatic Undo/Redo and storing its full state in localStorage. And here is running transpiled version (it writes to your browsers local storage but no information is sent to the server side). I’m not giving many details in this article, it is about async generators without dependency, but I suppose the code is straightforward to read. Check for example undoredo.js for easy time traveling implementation details.

这是GitHub上的摘要示例 ,具有上述所有功能以及自动撤消/重做功能,并将其完整状态存储在localStorage 。 并且这里运行的编译版本(它会写入浏览器的本地存储,但不会将任何信息发送到服务器端)。 在本文中,我没有提供太多细节,它是关于没有依赖关系的异步生成器的,但是我想代码很容易阅读。 例如,查看undoredo.js以获得轻松的时差实施细节。

The original sample requires almost no changes, I only replaced not serializable Promises, with corresponding functions from “es-persist” and replaced closures with invocations of R.bindfunction from the same library. EffectfulJS toolchain has another transpiler to make all the functions, including closures serializable, but not used in this example to keep it simpler.

原始示例几乎不需要进行任何更改,我只替换了不可序列化的Promises,使用了来自“ es-persist”的相应函数,并使用了从同一库中调用R.bind函数替换了闭包。 EffectfulJS工具链还有另一个编译器,可以使所有功能(包括闭包可序列化),但在本示例中并未使用它来使其更简单。

The story is just a brief description of the technique. I’m using it for a couple of years already, and happy because of improvements it provides. Try it, and I’m sure you’ll enjoy it too. There are many things to describe in depth. Stay tuned!

故事只是对该技术的简要说明。 我已经使用了两年了,由于它提供了改进,所以很高兴。 试试吧,我相信您也会喜​​欢它。 有很多东西需要深入描述。 敬请关注!

翻译自: https://www.freecodecamp.org/news/async-generators-as-an-alternative-to-state-management/

异步生成器

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值