如何在不使用库的情况下在React应用程序中组织一般状态(以及为什么需要mobx)

立即,一个小的破坏者-在mobx中组织状态与在纯反应中不使用mobx来组织一般状态没有什么不同。 对于自然问题的答案是,为什么为什么在本文结尾处需要这个笨拙的东西,但是现在,本文将致力于在没有任何外部库的情况下在干净的React应用程序中进行状态组织。




react提供了一种使用类组件实例上的state属性和setState方法存储和更新组件状态的方法。 然而,在反应社区中,使用了许多其他的与状态一起使用的库和方法(flux,redux,redux-ations,效应子,mobx,大脑等)。 但是,是否有可能仅使用setState使用一堆具有大量实体以及组件之间的复杂数据关系的业务逻辑来构建足够大的应用程序? 是否需要其他库与状态一起使用? 让我们弄清楚。

因此,我们有了setState,它会更新状态并调用组件的渲染器。 但是,如果许多未互连的组件需要相同的数据怎么办? 在React的官方平台中,有一个“提升状态”部分,其中有详细说明-我们只需将状态(提升至这些组件所共有的祖先),通过props(以及必要时通过中间组件)数据和用于更改它的函数即可。 对于较小的示例,这看起来是合理的,但是现实情况是,在复杂的应用程序中,组件之间可能存在很多依赖关系,并且倾向于将状态转移到祖先的公共组件中,这导致整个状态越来越高,最终将与App一起出现在App的根组件中用于更新所有组件状态的逻辑。 结果,只会发生setState来更新本地或在App的根组件中的数据组件,所有逻辑都将集中在其中。


但是是否可以在不使用setState或任何其他库的情况下将进程和呈现状态存储在React应用程序中,并提供从任何组件对该数据的常规访问?


最常见的javascript对象以及用于组织它们的某些规则对我们有所帮助。


但是首先,您需要学习如何将应用程序分解为实体类型及其关系。


首先,我们引入一个对象,该对象将存储适用于整个应用程序的全局数据-(可以是样式,本地化,窗口大小等的设置)在单个AppState对象中,并将此对象放入单独的文件中。


// src/stores/AppState.js export const AppState = { locale: "en", theme: "...", .... } 

现在,您可以在任何组件中导入和使用我们商店的数据。


 import AppState from "../stores/AppState.js" const SomeComponent = ()=> ( <div> {AppState.locale === "..." ? ... : ...} </div> ) 

我们走得更远-几乎每个应用程序都具有当前用户的本质(无论如何创建或来自服务器等),因此我们用户的单例对象也将处于应用程序状态。 也可以将其移动到单独的文件中,也可以将其导入,也可以将其立即存储在AppState对象中。 现在最主要的是-您需要确定组成应用程序的实体的图。 就数据库而言,这些将是具有一对多或多对多关系的表,并且整个关系链都从用户的主要本质开始。 嗯,在我们的例子中,用户的对象将只存储其他对象-实体-存储的数组,而每个对象存储又将存储其他实体-存储的数组。


这是一个示例-业务逻辑表示为“用户可以创建/编辑/删除文件夹,每个文件夹中的项目,每个任务项目和每个子任务任务中的项目”(结果类似于任务管理器),并将在状态图中查找像这样的东西:


 export const AppStore = { locale: "en", theme: "...", currentUser: { name: "...", email: "" folders: [ { name: "folder1", projects: [ { name: "project1", tasks: [ { text: "task1", subtasks: [ {text: "subtask1"}, .... ] }, .... ] }, ..... ] }, ..... ] } } 

现在,应用程序的根组件可以简单地导入该对象并呈现有关用户的一些信息,然后可以将用户对象转移到仪表板组件


  .... <Dashboard user={appState.user}/> .... 

他可以呈现文件夹列表


  ... <div>{user.folders.map(folder=><Folder folder={folder}/>)}</div> ... 

文件夹的每个组件将显示项目列表


  .... <div>{folder.projects.map(project=><Project project={project}/>)}</div> .... 

项目的每个组件都可以列出任务


  .... <div>{project.tasks.map(task=><Task task={task}/>)}</div> .... 

最后,每个任务组件可以通过将所需对象传递给子任务组件来呈现子任务列表


  .... <div>{task.subtask.map(subtask=><Subtask subtask={subtask}/>)}</div> .... 

自然地,在一页上,没有人会显示所有文件夹的所有项目的所有任务,它们将被侧面板(例如,文件夹列表),页面等划分。但是总体结构是这样的-父组件使嵌入的组件通过props传递对象。数据。 应该注意一个重要的点-任何对象(例如,文件夹,项目,任务的对象)都不会存储在任何组件的状态内-组件只是通过道具将其作为更通用对象的一部分接收。 例如,当项目组件将Task对象( <div>{project.tasks.map(task=><Task task={task}/>)}</div> )传递给Task的子组件时,由于这些对象存储在单个对象中您可以随时从外部更改此任务对象-例如,AppState.currentUser.folders [2] .projects [3] .tasks [4] .text =“ edited task”,然后调用以更新根组件(ReactDOM.render(<App /> ),并以此方式获取应用程序的当前状态。


进一步假设我们要在“任务”组件中单击“ +”按钮时创建一个新的子任务。 一切都很简单


  onClick = ()=>{ this.props.task.subtasks.push({text: ""}); updateDOM() } 

因为Task组件接收一个任务对象作为道具,并且该对象没有存储在其状态内部,而是属于全局AppState存储的一部分(也就是说,该任务对象存储在更通用的项目对象的任务数组内部,而该对象又是用户对象的一部分,而用户已经存储在AppState内部),并且由于这种连通性,在将新的任务对象添加到subtasks数组之后,您可以调用根组件更新,从而只需调用upd函数即可更新和更新所有数据更改(无论它们发生在哪里)的内部。 ateDOM,它反过来只是更新根组件。


 export function updateDOM(){ ReactDom.render(<App/>, rootElement); } 

而且,AppState的哪些部分以及从何处更改的数据都无关紧要(例如,您可以通过props通过中间的Project和Task组件将文件夹对象转发到Subtask组件,它可以更新文件夹名称(this.props.folder.name =“新名称”)-由于组件通过prop接收数据,因此更新根组件将更新所有嵌套的组件并更新整个应用程序。


现在,让我们尝试为侧面操作添加一些便利。 在上面的示例中,您会注意到,如果对象具有许多带有默认参数的属性,则每次创建一个新的实体对象(例如project.tasks.push({text: "", subtasks: [], ...})列出它们,您可能会犯错并且忘记一些东西,等等。首先想到的是将对象的创建放入一个函数中,在该函数中将分配默认字段,同时用新数据重新定义它们


 function createTask(data){ return { text: "", subtasks: [], ... //many default fields ...data } } 

但如果从另一面看,此函数是某个实体的构造函数,而javascript类非常适合此角色


 class Task { text: ""; subtasks: []; constructor(data){ Object.assign(this, data) } } 

然后创建对象将仅创建具有重写某些默认字段的能力的类的实例


 onAddTask = ()=>{ this.props.project.tasks.push(new Task({...}) } 

此外,您会注意到,以相同的方式,为项目对象,用户,子任务创建类,我们在构造函数中获得了代码重复


 constructor(){ Object.assign(this,data) } 

但是我们可以利用继承的优势,并将此代码放入基类的构造函数中。


 class BaseStore { constructor(data){ Object.update(this, data); } } 

此外,您会注意到,每次更新某些状态时,我们都会手动更改对象的字段


 user.firstName = "..."; user.lastName = "..."; updateDOM(); 

跟踪,讨价还价和了解组件中发生的事情变得很困难,因此需要确定一个公共通道,任何数据的更新都将通过该通道通过,然后我们可以添加日志记录和各种其他便利设施。 为此,解决方案是在类中创建一个update方法,该方法将使用具有新数据的临时对象并进行自我更新,并建立规则,即只能通过update方法而不通过直接分配来更新对象


 class Task { update(newData){ console.log("before update", this); Object.assign(this, data); console.log("after update", this); } } //// user.update({firstName: "...", lastName: "..."}) 

好吧,为了不重复每个类中的代码,我们还将此更新方法移至基类。


现在您可以看到,当我们更新某些数据时,我们必须手动调用updateDOM()方法。 但是,为方便起见,每次调用基类的update({...})方法时,都可以自动执行此更新。
事实证明,基类看起来像这样


 class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); ReactDOM.render(<App/>, rootElement) } } 

好吧,这样在后续调用update()方法的过程中就不会有不必要的更新,您可以将组件更新延迟到下一个事件周期


 let TimerId = 0; class BaseStore { constructor(data){ Object.update(this, data); } update(data){ Object.update(this, data); if(TimerId === 0) { TimerId = setTimeout(()=>{ TimerId = 0; ReactDOM.render(<App/>, rootElement); }) } } } 

此外,您可以逐渐增加基类的功能-例如,为了不必每次都手动将请求发送到服务器,除了更新状态外,还可以在后台将请求发送到update({..})方法。 您可以通过在全局哈希图中添加每个已创建对象的帐户来组织Web套接字的实时更新通道,而无需更改组件并以任何方式使用数据。


还有很多事情要做,但是我想提一个有趣的话题-通常将带有数据的对象传递给必要的组件(例如,当项目组件呈现任务组件时)


 <div>{project.tasks.map(task=><Task task={task}/>)}</div> 

任务的组成部分可能需要一些未直接存储在任务内部但位于父对象中的信息。


假设您要用项目中存储的所有任务通用的颜色为所有任务着色。 为此,除了任务道具外,项目组件还必须传输其项目道具<Task task={task} project={this.props.project}/> 。 而且,如果您突然需要用一个文件夹中所有任务通用的颜色为任务上色,则必须将当前文件夹对象从“文件夹”组件传输到“任务”组件,方法是通过中间项目组件将其转发。
脆弱的依赖关系似乎表明该组件应该知道其嵌套组件需要什么。 而且,反应上下文的可能性尽管将简化通过中间组件的传输,但仍将需要提供者的描述以及子组件需要哪些数据的知识。


但是主要的问题是,每当您需要某个组件需要新信息时,您每次编辑设计或更改客户的心愿单时,您都将不得不更改更高的组件,即转发道具或创建上下文提供者。 我希望组件通过props接收带有数据的对象,以某种方式访问​​应用程序状态的任何部分。 在这里,javascript非常适合(与其他功能语言(如elm或不可变的方法,如redux)不同)–对象可以存储彼此之间的循环链接。 在这种情况下,任务对象应具有task.project字段,该字段具有指向存储该父项目的父项目的对象的链接,而该项目对象又应具有与文件夹对象等的链接,也指向根AppState对象。 因此,无论组件有多深,它始终可以通过链接遍历父对象并获得所有必要的信息,而无需将其扔给一堆中间组件。 因此,我们引入了一条规则-每次创建对象时,您都需要向父对象添加链接。 例如,现在创建一个新任务将如下所示


  ... const {project} = this.props; const newTask = new Task({project: this.props.project}) this.props.project.tasks.push(newTask); 

此外,随着业务逻辑的增加,您可以注意到,锚定板与反向链接支持相关联(例如,在创建新对象时或例如将项目从一个文件夹转移到另一个文件夹时,将链接分配给父对象,您不仅需要更新project.folder = newFolder属性并删除您自己从上一个文件夹的项目数组中添加一个新文件夹到项目数组)开始重复,它也可以移动到基类,以便在创建对象时足以指定父项- new Task({project: this.porps.project}) new Task({project: this.porps.project})和基类会自动将新对象添加到project.tasks数组,并且在将任务转移到另一个项目时,仅更新task.update({project: newProject})字段就足够了,基类会自动从中删除任务一系列来自先前项目的任务,并已添加到新项目中。 但这已经需要声明关系(例如,在静态属性或方法中),以便基类知道要更新的字段。


结论


以这种简单的方式,仅使用js对象,我们得出的结论是,您可以获得使用应用程序一般状态的所有便利,而无需在应用程序中引入对使用状态的外部库的依赖。


问题是,为什么我们需要库来管理状态,尤其是mobx?


事实是,在描述的一般状态组织方法中,当使用普通的原生“香草” js对象(或类对象)时,存在一个很大的缺点-当一小部分状态甚至一个字段发生更改时,组件将被更新或“呈现”,而这些组件不会以任何方式连接并且不依赖于该部分状态。
在具有粗体ui的大型应用程序上,这会导致刹车,因为react根本没有时间来递归比较整个应用程序的虚拟房屋,因为除了比较每个渲染器之外,每次都会生成一个新的对象树来描述绝对所有组件的布局。


但是,尽管这个问题很重要,但它纯粹是技术性的-存在类似于虚拟dom反应的库,可以更好地优化渲染器并增加组件限制。


比创建新的虚拟家庭树和随后的与前一棵树的递归比较传递相比,有更多有效的家庭装修技术。


最后,有些库试图通过另一种方法来解决更新缓慢的问题-即跟踪状态的哪些部分连接到哪些组件以及更改某些数据时,仅计算和更新依赖于此数据的那些组件,而不接触其余组件。 Redux也是这样一个库,但是它需要一种完全不同的状态组织方法。 但是相反,mobx库并没有带来任何新的东西,我们实际上可以在不更改应用程序的任何情况下加快渲染器的速度-只需将@observable装饰器添加到类的字段中,并将@observable装饰器添加到渲染这些字段的组件中,即可只需在基类的update()方法中为根组件删除不必要的更新代码,我们将获得一个可以正常运行的应用程序,但是现在更改状态的一部分甚至是一个字段都将仅更新那些组件 其中成熟签署(车削内部方法渲染())为对象的特定状态的特定字段。

Source: https://habr.com/ru/post/zh-CN416361/


All Articles