llllll

React 中的状态架构模式

原文链接: medium.com

这是一系列文章中的第一篇,其目的是提供对使用 React(或者是类 React 的库)构建复杂 Web 应用时使用的一些常见架构模式的深入审查,以及提供一些建议,用来避免使用那些模式时经常会遇到的问题。

我承认我还有第二个目的。 在我写这篇文章的时候,我意识到我在构建应用程序时使用的一些模式可以被分解成一个单独的库,从而自动化形成一些好的架构实践。 所以这篇文章的另一个动机是介绍这个库 zine,如果可以的话,希望你能看看,玩一下, 甚至可能给我一些关于它的反馈。

写这一系列文章的总的原因是复杂的React应用程序存在特定的架构问题: 在管理跨不同组件层次结构的状态依赖关系时,并且不会造成很多不必要的复杂性和低效率,是很困难的。

在第一部分中,我将介绍一些非常基本的(大概是耳熟能祥的)概念,以便解开上述的索引,然后我将讨论构建React应用程序的最常见的方法 - 我称之为naive hierarchical分层架构 。最后我将描述使用这种架构模式时出现的复杂性问题。

如果您是 React 的新手,请仔细阅读第一部分。不是的话,略读就好,但确保明白我所说的 managementdistributionpublishing,并且在开始之前,你能在你的经验中认识到交叉依赖关系造成的复杂性问题。

第二部分,我将讨论另外一种可以选择的架构模式,我把它称为 top-heavy architecture。 在讨论可能出现在top-heavy architecture中的性能问题之前,我会简要论述 top-heavy architecture与另一种称为 Flux 的模式之间的关系。

即使你以前使用过 top-heavy 或 Flux-y 框架,第二部分可能还是值得一看的,特别是在最后讨论的实际的性能问题。

第三部分 引入了一个先进的架构模式,通过引入我称之为articulation points的模式来精简 top-heavy 结构。 我描述了这种方法,并论述如何使用zine库直接实现它,然后根据前三个部分所涵盖的内容概述整体架构策略。

最后,第四部分讨论了对基于 zine的特定架构的的一些深入改进。例如,集成 Flux,使用不可变数据并采用一个更具声明性,基于数据流的状态管理系统。

但是现在我们来开始一些基础知识。

状态依赖和应用架构

state 使得 Web 应用程序复杂起来。状态是后果的变化。 静态网页不会改变(并不是特别复杂),但动态Web应用可以重新配置其接口以响应内部变量和数据结构中的状态变化。

在本文中,我将主要使用“information”来表示state information,或者是指传达给一个东西关于另一个东西状态的信息。

所有动态组件需要持续获取关于状态的确定信息来进行渲染。切换控件需要知道它是否被切换,当该状态改变时,电子表格单元需要知道其内容以及何时更新这些内容等。

一个组件依赖的状态是叫做状态依赖。 状态依赖应该与组件本身的DOM状态或视觉状态区别开来。 切换控件管理的DOM可以处于表示“开”和“关”的不同状态,但实际的布尔值确定其所处的状态是切换控件的状态依赖关系。

组件通常可以被认为是其状态依赖性的功能。 尽管通常会在 React 中区分 props 和 state 是,但实际上 props 是一种状态依赖的形式 - 它们是组件用于确定要渲染的值。 也就是说,区分 this.propsthis.state 仍然是有用的,根据前者是额外的赖,后者是内部依赖的事实。

我们经常根据它们是否具有任何内部状态来区分有状态和无状态组件。如果以表面来看,这个术语可能会令人困惑。 这不是指有状态的组件可以改变,无状态的组件不会改变 - 正如我们所提到的,基本上所有的用户界面组件都可以处于不同的状态并具有状态依赖性。区别实际上取决于是怎么通知组件的状态以及该信息来自哪里。

如果一个组件的状态是仅在其内部的,这就是说它由组件管理的内部变量来决定,那么这个状态指存活在该组件中,组件被称为“有状态”。 如果组件没有任何内部状态,并且其状态完全由外部传达给它的因素决定(例如 props),那么我们将其称为“无状态”。 无状态组件没有任何隐藏的状态 - 它们是对外部依赖关系的静态模数更改。 有状态的组件通常被认为是可变对象,而无状态组件更像纯函数。

在开发基于组件的应用程序时,我们做出的许多选择都归结于 state 应该存在的什么地方,以及如何被传递。

特别要说的是,对于任何状态数据,有三个问题要回答:

  • 什么在管理这些数据 (fetches/stores/updates/etc.)?

  • 在初始化时,怎样分发(distribution)到依赖它的组件里?

  • 怎么将更新发布(publishing)到组件里?

在回答这些问题时,我们做出的选择决定了我们应用的 information architecture

在这里区分 distributionpublishing 特别重要。 通过 props 将状态依赖关系传递给组件,将其分配到其初始状态,但在组件渲染后,不影响该依赖关系的状态更改不会影响组件,直到以某种方式发布到组件(例如,通过用新的 props 重新渲染它,或者调用this.setState)。

React 的 UI 组件层级

我们刚刚讨论的信息架构与用户界面组件的层次结构是不同的(尽管有时会混淆他们)。

React 组件提供声明性的方式来构建一个DOM树。React 组件代替和管理 DOM 树的部分,因此它们被排列成与 DOM 结构对齐的树状层次结构。 从根节点开始,每个 React 组件通过声明子节点(更多的 React 组件或虚拟 DOM 标记元素)并将 props 传递给他们来呈现自身。

React 和 State Distribution

React 强烈地提出了一个在回答关于信息架构决策时出现的至少一个问题的约定:当将消息传递给组件时,应该通过 props 在渲染时通过组件层次结构传递。 状态信息的传递基本上总是通过(并且因此产生耦合)组件层级来做的。

在 React 应用中,状态信息倾向于通过 props 按照组件层次结构从父级到子级流动。 以任何其他方式传递信息通常要复杂得多。 特别是很难从子级到父级来传递信息(通常通过回调来实现),在兄弟组件之间更难。在短期内就会显示重要的影响。

在本文的其余部分中,我们假设当组件不管理自己的状态时,相关信息将通过 props 传递给他们。

所以我们再回想一下另外两个架构问题:我们如何管理状态,我们如何发布状态的变化?

朴素分层架构模式(naive hierarchical architecture)

也许最常见的组织 React 应用的方法是使用我称之为的朴素分层架构(naive hierarchical architecture),其中主要驱动是组件管理自己的状态。 在 React 中,这是通过内部状态变量 _this.state 及其 setter 方法 this.setState 来实现的。

由于使用this.setState更新this.state并触发一次重新渲染 ,它可以通过 props 有效地将任何更改按照层次结构发布到子级(最终到DOM),从而将状态管理和更新归结为单一事件处理。

通常看到一个组件管理自己的数据,例如 通过在生命周期方法中的挂载阶段从服务器获取数据,将其保持在内部状态,并在事件处理程序内部调用this.setState以将其更改并将更改发布到层次结构中。

这很简单,在很多情况下都能很好地工作。 朴素层次架构的主要特征是状态数据耦合到特定的用户界面组件非常严重。

这可以是一种力量,因为它有助于在许多情况下进行封装。自管理组件通常非常独立,当与获取,处理和显示数据相关的所有内容都位于同一位置时,意大利面式的代码较少。 但是我们会看到,它也可能成为一个问题。

当没有很多共享信息依赖关系时,朴素层次架构效果最好 - 如果只需要了解一些状态数据就只是当前组件(或直接子级),那么这个架构对于这个组件管理状态很有意义。 在下一节中,我们将讨论当不是这种情况时可能出现的一些问题。

在讨论与朴素层次架构相关的缺点之前,我应该给出一个小的免责声明:如果你只是制作一个独立的组件,或者你的应用程序很简单,那么你一定要考虑从朴素层次架构开始。这通常很好。可替代的架构可能是过度的,应该用于解决当您的应用变得更加复杂时出现的特定问题。

在朴素方法中:层级 vs 架构

与组件管理自己的状态数据相关联的一个缺点是将状态管理与组件生命周期相耦合。 如果某些数据存在于组件的本地状态中,那么它将与该组件一起消失 - 没有进一步保存数据,那么只要组件卸载,state 的内容就会丢失。这可能导致各种问题,例如,不必要地重复的数据提取和界面一旦不可见就恼人地忘记状态的情况。

但是,自管理组件的主要问题是将信息架构的所有方面(管理,分发和发布)与用户界面组件层级结构相耦合。

组件层次结构的形状在很大程度上取决于通过 DOM 结构的页面布局。 因为信息主要通过React中的 props 分发,组件之间的父/子关系的结构(由页面布局确定)强烈地影响在它们之间传递信息的容易程度,因此单向(父到子)对信息分配的约束可能会对您如何管理和发布状态产生奇怪的影响。

组件之间的状态依赖关系的结构应该与DOM的层次结构相匹配是无脑的,当这两个不一致时,事情就会变得复杂起来。

当你拥有管理某些状态的组件并需要将其传递给其后代时,事情是非常简单的。 在这种情况下,管理该状态的组件可以直接通过 props(如果依赖组件是直接子代)或在必要情况通过中间人将相关信息传递给依赖组件。

因为在 React 中通过层次结构中传递信息是最简单的方法,所以我们倾向于通过父组件管理状态,并根据需要将其传递给子组件来考虑事情。 但有时,DOM 的结构要求子组件需要参与管理状态。例如,如果你有一个影响包含该组件的组件状态的按钮。在这种情况下,会被诱惑去找到一种将信息从子级传递到父级的方式。

这通常通过回调来完成。 例如,父组件可以传递一个子切换控件的切换状态(“on”/“off”)和一个在触发切换被触发时调用的处理程序,这触发了父进程中的状态改变和新状态会触发组件重新渲染。

但祖先/后代的关系只会让我们到目前为止。 并非所有组件都将以这种方式相关。 如果两个兄弟组件都依赖于一些状态信息,那么即使父级组件没有其他理由关心该信息,他们也必须从共同父级来获取它们。

这种状态依赖模式是我所说的cross-cutting,因为它跨越了组件层次结构。 将状态从一个组件移动到其父级以便与兄弟分享它通常不是很大的事情,但它说明了一个关于朴素层次结构的基本观点:它迫使开发人员决定如何管理和管理状态 并且分布式依赖于DOM的结构。 此外,当DOM的结构与信息依赖性的结构不完全一致时,它就会提高复杂度。

最近的共同祖先被迫处理横跨层次结构的共享状态依赖关系

如果组件层次结构更复杂,并且依赖关系越大越大,事情就会变得更糟。 在朴素层次架构下,如果页面的完全独立部分上的两个组件和组件层次结构的完全不同的分支共享状态依赖关系,则必须通过其最接近的共同祖先将它们(以某种方式或另一种方式)传递给它们。

这是一个问题,原因有很多。 这是一个复杂度的来源,在 the Hickean sense:更多的东西是相互交织的,相互依赖的比他们需要的更多。 它不必要地将应用程序的逻辑和信息流耦合到组件层次结构(和页面的布局中),并且使应用程序的状态结构难以预测。 如果你想看到管理状态逻辑的一些代码,那么就不容易找到它。 你可能会从你关心的 state 影响的组件开始,并且无法找到管理它的任何逻辑。 相反,你发现它从父组件传入,并且你必须基本上处理组件层次结构,直到找到管理你关心的 state 的组件。可能难以准确地预测这个 state 在层次结构中的哪里。

一个相关的问题是,这种架构是不稳定的,通常需要重构。 如果视觉布局发生变化,那么DOM结构和组件层次结构就会发生变化,而且由于信息流和状态逻辑是绑定到上面得,所以它们往往也必须改变。 你可以将状态逻辑构建到组件中,然后找出其中的一个兄弟姐妹也需要该信息,那么就把逻辑移到层次结构之上,然后突然发现页面其他部分的另一个组件也需要该信息,你不得不再次重构。

布局更改可以强制状态向上迁移

简而言之,朴素分层架构的问题是交叉信息依赖会让事情变得混乱。

那么有其他架构不是那么混乱,而且更容易适应跨组件信息依赖? 当然。 我们将在下一期中介绍。