反应心理模型的视觉指南:使用状态、使用效果和生命周期

2020-05-29 21:04:25

我喜欢心理模型。它们对于理解复杂系统至关重要,使我们能够直观地把握和解决复杂问题。

这是关于反应心理模型的三篇系列文章中的第二部分。我将通过从头开始构建复杂的反应组件,并使用大量的可视化解释,向您展示我在复杂反应组件中使用的确切心理模型。

我建议您先阅读第1部分,因为本文中的心理模型依赖于我在那里解释过的模型。如果您需要复习一下,这里是第1部分的完整心智模型。

无论你已经使用Reaction多年还是刚刚开始,在我看来,拥有一个有用的心理模型是让你对使用它感到自信的最快方式。

组件的生命周期:挂载、渲染、卸载:许多错误的来源是缺乏一个良好的心理模型来解决这些问题。

心智模型是一种思维过程或心理形象,它帮助我们理解复杂的系统,并通过引导我们朝着正确的方向直观地解决难题。你每天都在使用心理模型;想想你想象互联网、汽车或免疫系统是如何工作的。你与每个复杂的系统互动都有一个心理模型。

下面是我在第1部分中解释的反应心理模型的一个非常快速的概述,或者您可以在这里找到第1部分的完整版本。

Reaction组件就像一个函数,它接收道具,这些道具是函数的参数,每当这些道具改变时,它都会重新执行。我将一个组件想象成一个位于另一个盒子中的盒子。

每个长方体可以有多个子项,但只能有一个父项,除了从其父项接收道具外,它还具有名为state的内部特殊变量,这也使其在更改时重新执行(重新渲染)。

我在第1部分中介绍了state的工作原理,以及它是框中的一个特殊属性。与每次渲染时重新声明的变量或函数不同,退出使用状态的值在渲染之间总是一致的。它们在挂载时使用defaultvalue进行初始化,并且只能通过设置状态事件进行更改。

但是,如何应对才能防止状态在每次呈现时丢失其值呢?答案是范围。

我在PAT 1中解释了闭包和作用域的心理模型。简而言之,闭包就像一个半透水的盒子,让来自外部的信息进入,但不会泄漏任何东西。

使用useState,Reaction将其值限定为最外层的闭包,即包含所有组件的React应用程序。换句话说,无论何时使用useState,REACT都会返回一个存储在组件外部的值,因此每次呈现时都不会更改。

Reaction通过跟踪每个组件和声明每个钩子的顺序来做到这一点。这就是为什么你不能有一个反应胡金赛德一个条件。如果有条件地创建useState、useEffect或任何其他挂钩,则Reaction无法正确跟踪它。

每当重新呈现组件时,useState要求获取当前组件的状态,然后Reaction检查包含每个组件的所有状态的列表,并返回相应的状态。该列表存储在组件之外,因为在每次重新渲染时都会创建和销毁变量和函数。

虽然这是一个关于国家如何运作的技术观点,但通过理解它,我可以将Reaction的一些魔力转化为我可以想象的东西。对于我的心理模型,我会把事情简化成一个更简单的想法。

我在使用useState时的心理模型是这样的:因为状态不受框中发生的事情的影响,所以我将其想象为init中的常量值。我知道无论发生什么情况,在我的组件的整个生命周期中,状态都将保持一致。

一旦我们了解了状态是如何保持的,了解它是如何变化的就很重要了。

您可能知道状态更新是异步的,但这意味着什么呢?这对我们的日常工作有什么影响?

同步代码会阻止您的应用程序运行的JavaScript线程执行任何其他工作。线程中一次只能运行一段代码。

异步代码不会阻塞线程,因为它会被移到队列中,只要有时间就会运行。

我们使用STATE作为变量,但是更新它是异步的。这很容易落入认为设置状态会像变量一样立即更改其值的陷阱,这会导致错误和挫败感,例如:

const component=()=>;{const[searchValue,setSearchValue]=useState(';';);//当用户在输入const handleInput=e=>;{//将值保存在状态中,然后使用它来获取新数据❌setSearchValue(e.target.value);fetchSearch(SearchValue)时,搜索内容。则(Results=>;{//做某事});};};

这个代码有错误。想象一个人输入“再见”。代码将搜索BY而不是BYE,因为每个新笔画都会触发一个新的setSearchValue和fetchSearch,但是因为状态更新是异步的,所以我们将使用过时的searchValue进行提取。如果一个人打字足够快,而我们正在运行其他JavaScript,我们甚至可以搜索b,因为JavaScript还没有时间运行队列中的代码。

长话短说,不要指望状态会立即更新。这样可以修复错误:

const component=()=>;{const[searchValue,setSearchValue]=useState(';';);const handleInput=e=>;{//将搜索保存在变量中使其可靠✅const search=e.target.value;setSearchValue(Search);fetchSearch(Search)。则(Results=>;{//做某事});};};

状态更新是异步的原因之一是对其进行优化。如果一个不同状态的应用程序想要一次更新,那么Reaction会尝试将尽可能多的状态批处理到单个异步操作中,而不是运行多个同步操作。一般来说,异步操作的性能也更高。

另一个原因是一致性。如果一个状态快速连续更新多次,为了保持一致性,REACTION将只采用最新的值。如果更新是同步并立即执行的,则很难做到这一点。

在我的心理模型中,我认为个人状态价值是可靠的,但速度很慢。每当我更新一个,我知道它可能需要一段时间才能改变。

但是,当组件安装和卸载时,状态和组件本身会发生什么呢?

我们过去经常谈论生命周期方法,因为只有类组件才能访问状态并控制组件在其生命周期内发生的事情。但自从胡克斯问世并允许我们使用同样的功能部件后,这个想法就变得不那么重要了。

有趣的是,每个组件仍然有一个生命周期:挂载、呈现和卸载,每个步骤都必须考虑到Reaction组件周围功能齐全的心智模型。

所以让我们来看看每个阶段,并为它建立一个心理模型,我保证这会让你更好地理解一个组件。

当React第一次创建或呈现一个组件时,它正在挂载它,这意味着它将被添加到DOM中,并且Reaction将开始跟踪它。

我喜欢想象挂载为一个新的盒子,或者将其添加到其父盒子的内部。

只要组件尚未呈现,并且其父级决定第一次呈现它,就会进行挂载。换言之,坐骑就是“诞生”的组成部分。

一个组件可以多次创建和销毁,每次创建时都会再次挂载。

const component=()=>;{const[show,setShow]=useState(False);return(Show Menu//挂载with show=true,unomunted with show=false{show&;&;});};

Reaction呈现组件的速度如此之快,以至于看起来像是在隐藏它们,但在现实中,它正在非常快速地创建和删除它们。在上面的示例中,每次单击按钮时都会在DOM中添加和删除<;MenuDropdown/>;组件。

请注意组件的父级是如何决定何时装入和卸载<;MenuDropdown/>;的。这在层次结构中也是向上的。如果MenuDropdown包含子组件,则它们也将被挂载或卸载。组件本身永远不知道何时安装或卸载。

请注意,useEffect挂钩在初始呈现之后运行。这时您需要运行代码,如创建事件侦听器、执行繁重逻辑或获取数据。有关这一点的更多信息,请参见下面的useEffect部分。

我挂载的心理模型是这样的:每当父框决定必须创建子对象时,它都会挂载它,然后组件将做三件事:为useState分配默认值、运行其逻辑、呈现和执行useEffect挂钩。

挂载阶段与正常的重新渲染非常相似,不同之处在于使用默认值初始化useState,并且元素是第一次添加到DOM中。挂载后,组件将保留在DOM中,并会进一步更新。

一旦一个组件被挂载,它将继续存在,直到它被卸载为止,在这两者之间进行任何数量的渲染。

我在第1部分中解释了呈现心智模型,但让我们简要回顾一下,因为这是一个重要的阶段。

组件挂载后,对道具或状态的任何更改都会导致它重新呈现,重新执行其中的所有代码,包括其子组件。在每次呈现之后,将再次计算useEffect挂钩。

我把一个组件想象成一个盒子,它的重新渲染能力使它成为一个可重用的盒子。每次渲染都会回收长方体,它可以输出不同的信息,同时保持底层的状态和代码不变。

一旦组件的父级决定停止渲染子组件-由于条件、数据更改或任何其他原因-该组件将需要卸载。

卸载组件时,REACT会将其从DOM中删除,并停止跟踪它。该组件将被删除,包括其具有的任何状态。

如安装阶段所述,组件既由其父级挂载又由其父级卸载,如果该组件反过来具有子级,它也会卸载这些子级,并重复此循环,直到到达最后一个子级。

在我的心理模型中,我认为这就像是父盒在捣毁子盒。如果你把一个容器扔到垃圾桶里,里面的所有东西都会被扔到垃圾桶里,这包括其他的盒子(组件),状态,变量,所有的东西。

但是组件可以在其外部创建代码。由要卸载的组件创建的任何订阅、Web套接字或事件侦听器会发生什么情况?

答案是什么都没有。这些函数在组件外部运行,不会受到删除组件的影响。这就是为什么在卸载之前,组件在自身之后进行清理是很重要的。

每个函数都会耗尽资源。如果不对其进行清理,可能会导致严重的错误、性能下降,甚至存在安全风险。

我认为这些功能就像是在我的盒子外面转动的齿轮。它们在组件挂载时设置为InMotion,在组件卸载时必须停止。

我们可以通过useEffect的返回函数清理或停止这些齿轮。我将在效果挂钩部分详细解释。

到目前为止总结一下:组件只是一个函数,道具是函数的参数,状态是一个特殊的值,Reaction确保在渲染过程中保持一致。所有组件都必须位于其他组件中,并且每个父级都可以在其中包含多个子级。

在我的心智模型中,组件就是一个盒子,根据某种逻辑,它可以决定是创建还是删除一个子框。当它创建它时,一个组件被装载,当它删除它时,它被卸载。

盒子安装意味着它是被创建和执行的。这里是useState用默认值初始化的时候,Reaction呈现它以便用户可以看到它,并开始跟踪它。

挂载阶段是我们倾向于连接到外部服务、获取数据或创建事件侦听器的阶段。

一旦挂载,每当盒子的道具或状态改变时,它都会被重新呈现,我想这是因为盒子被回收了,除了状态之外的所有东西都会重新执行和重新计算。用户在每次新渲染时看到的内容可能会改变。重新渲染是第二个阶段,可以进行任意次数,不受限制。

一旦组件的父级决定删除它,无论是因为逻辑原因、父级本身被删除,还是数据更改,组件都将卸载。

当盒子卸载时,它会被丢弃,其中包含的所有内容都会被丢弃,包括子组件(子组件也会有自己的卸载)。这是我们有机会清理和删除在useEffect中初始化的任何外部函数的地方。

在您的应用程序中,挂载、重新渲染和卸载的循环可能会在您没有注意到的情况下发生数千次。反应速度快得令人难以置信,这就是为什么在处理复杂的组件时在脑海中保持一个心理模型是很有用的,因为很难实时看到正在发生的事情。

但是,我们如何利用代码中的这些阶段呢?答案是通过强大的useEffect钩子。

效果挂钩允许我们在组件中运行副作用。每当您获取数据、连接到服务或订阅或手动操作DOM时,都会产生副作用(也称为简单效应)。

函数上下文中的副作用是任何会使函数不可预测的东西,比如数据或状态。一个没有副作用的函数将是可预测的和纯粹的-你可能听说过纯函数-只要输入保持不变,总是做完全相同的事情。

效果挂钩始终在每次渲染后运行。原因是副作用可能包含繁重的逻辑或耗费时间,例如获取数据,因此一般来说,它们最好在渲染之后运行。

钩子接收两个参数:要执行的函数和具有将在每次呈现后计算的值的数组,这些值称为依赖项。

//选项1-无依赖使用Effect(()=>;{//每次渲染后运行的繁重逻辑});//选项2-空依赖使用Effect(()=>;{//创建事件监听器、订阅,获取一次性数据},[]);//选项3-具有依赖关系A、B或C更改时获取数据。},[a,b,c]);

根据第二个参数,您有3个行为不同的选项。每个选项的逻辑是:

如果不存在,效果将在每次渲染后运行。此选项不常用,但在某些情况下很有用,例如每次渲染后需要进行繁重的计算。

如果是空数组[],则在挂载和第一次渲染之后,效果只运行一次。这对于创建事件侦听器等一次性效果非常有用。

值为[a,b,c]的数组使效果对依赖项求值,只要依赖项发生更改,效果就会运行。这对于道具或状态更改(如获取新数据)时的运行效果非常有用。

依赖项数组赋予useEffect它的魔力,正确使用它非常重要。必须包含useEffect中使用的所有变量,否则,效果将在运行时引用以前渲染的陈旧值,从而导致错误。

ESLint插件eslint-plugin-action-hooks包含许多有用的特定于钩子的规则,其中一个规则将在您错过useEffect中的依赖项时发出警告。

我最初的use Effect心智模型将其作为一个生活在其组件中的迷你盒子,根据依赖关系数组的使用,有三种截然不同的行为:如果没有依赖关系,则效果在每次渲染之后运行;如果是空数组,则仅在挂载之后运行;或者,如果数组具有值,则在依赖关系发生变化时运行。

useEffect还有另一个重要特性,它允许我们在运行新效果或卸载之前进行清理。

每次我们创建订阅、事件侦听器或打开的连接时,我们都必须在不再需要它们时将其清除,否则,我们会造成内存泄漏并降低应用程序的性能。

这就是useEffect派上用场的地方。通过从其中返回一个函数,我们可以在应用下一个效果之前运行代码,或者如果效果只运行一次,则在卸载组件之前运行代码。

//此效果将在挂载时运行一次,创建事件监听器//它将在卸载时执行返回函数,移除事件监听,清理✅useEffect(()=>;{const handleResize=()=>;setWindowWidth(window.innerWidth);Window);窗口。addEventListener(';resize';,handleResize);return()=>;窗口。remoteEventListener(';resize';,handleResize);},[]);//每当`pros.stream.id`更改useEffect(()=>;{const handleStatusChange=StreamData=>;{setStreamData(Stream Data);};StreamingApi)时会出现该效果。SubscribeToId(props.stream.id,handleStatusChange);//取消订阅当前ID后,再使用新ID return()=>;StreamingApi运行下一个特效。unscribeToId(props.stream.id,handleStatusChange);},[props.stream.id]);

我设想使用Effect作为组件中的一个小方框,与组件的逻辑并存。此框的代码(称为效果)仅在Reacthas呈现组件后运行,它是运行副作用或繁重逻辑的完美位置。

useEffect的所有魔力都来自它的第二个参数,即依赖数组,它可以从中获得三种行为:

空数组:特效只在初始渲染后运行,卸载前返回函数。

带值的数组:每当依赖项更改时,效果都会运行,并且urn函数将在新效果之前运行。

我希望你会发现我的思维模式很有用!把它们解释清楚是一项挑战。如果你喜欢阅读,请分享这篇文章,这就是我对❤️的全部要求。

这是一个分为三部分的系列的第二部分,下一部分也是最后一部分将涵盖更高层次的概念,如Reaction上下文,以及如何更好地考虑您的应用程序以防止常见的性能问题。

如果你想在第三部分出版时收到通知,你可以订阅我的时事通讯,以及我计划的更多“心智模型”文章(使用Effect Depth、Git等)。

我们对视觉辅助的理解要好得多,而网络上像这样的材料还不够多,所以我会创作更多类似的文章(随着我磨练自己的设计技能,😅会做得更好)。

在推特上讨论