为什么是反模式?

你好。9月,将在OTUS上立即开设几门有关JS开发的课程,即:JavaScript Developer。专业的JavaScript开发人员。Basic和React.js开发人员。预期这些课程的开始,我们为您准备了另一篇有趣的翻译,我们还提供免费注册有关以下主题的演示课程:





现在让我们继续本文。










当我开始学习React时,有些事情是我不了解的。我认为几乎每个熟悉React的人都在问同样的问题。我确信这是因为人们正在建立整个图书馆来解决紧迫的问题。这是几乎每个React开发人员似乎都关心的两个主要问题:



一个组件如何访问另一个组件中的信息(尤其是状态变量)?一个组件如何调用另一个组件中的函数?



总体而言,JavaScript开发人员(尤其是React开发人员)最近越来越倾向于编写所谓的纯函数。与状态更改无关的功能。不需要外部数据库连接的函数。与外部发生的事情无关的功能。



当然,纯函数是一个崇高的目标。但是,如果您正在开发一个或多或少复杂的应用程序,那么您将无法使每个函数变得干净。当然,必须至少创建一些其他组件某种程度上相关的组件。试图避免这种情况是荒谬的。组件之间的这些连接称为依赖关系



通常,依赖关系不好,最好仅在需要时使用。但是话又说回来,如果您的应用程序已扩展,某些组件必定会相互依赖。当然,React开发人员知道这一点,因此他们想出了如何使一个组件将关键信息或功能传递给其子组件的方法。



标准方法:使用道具传递价值



任何状态值都可以通过prop传递给另一个组件。任何功能都可以通过相同的道具传递给子组件。这就是后代如何知道树上存储了哪些状态值,并可能潜在地调用父组件中的动作的方式。当然,所有这些都是好的。但是React开发人员担心一个特定的问题。



大多数应用程序是分层的。在复杂的应用程序中,结构可以嵌套得很深。一般体系结构可能看起来像这样:



App→指ContentArea

ContentArea→→指MainContentArea

MainContentArea→→指MyDashboard

MyDashboard→→指MyOpenTickets

MyOpenTickets→→指TicketTable

TicketTable→→指序列→TicketRow

每个人TicketRow→指→TicketDetail



从理论上讲,该花环可以缠好很长时间。所有组件都是整体的一部分。更确切地说,是层次结构的一部分。但是这里出现一个问题:上面示例中



的组件可以TicketDetail读取存储在其中的状态值ContentArea吗?要么。组件可以TicketDetail调用其中的函数ContentArea吗?

这两个问题的答案都是肯定的。从理论上讲,所有后代都可以知道父组件中存储的所有变量。他们还可以调用祖先函数-但要注意的是。仅当此类值(状态或函数值)通过prop显式传递给后代时才有可能。否则,组件的状态或函数值将无法用于其子组件。



在小型应用程序和实用程序中,这没有特殊作用。例如,如果某个组件TicketDetail需要访问存储在中的状态变量,TicketRow则足以使该组件TicketRowTicketDetail通过一个或多个prop将这些值传递给其后代→当组件TicketDetail需要调用in中的函数时,情况也是如此TicketRow。组件TicketRow→将TicketDetail通过prop将此功能传递给其后代→ 。当树下的某个组件需要访问层次结构顶部的组件的状态或功能时,就会开始出现头痛。



为了解决React中的这个问题,传统上将变量和函数传递给所有级别。但是,这会使代码混乱,占用资源并需要认真计划。我们将不得不值传递到很多层面是这样的:



ContentArea→交通MainContentArea→交通MyDashboard→交通MyOpenTickets→交通TicketTable→交通TicketRow→交通TicketDetail



也就是说,为了从通过状态变量ContentAreaTicketDetail,我们需要做大量的工作。经验丰富的开发人员了解到,通过中间层的组件以道具的形式传递价值和功能的丑陋链条很长。该解决方案非常麻烦,因此我甚至放弃了几次学习React的工作。



名为Redux的怪物



我并不是唯一一个认为通过props传递所有状态值和组件通用的所有功能的人不切实际的。您不太可能会找到一个没有状态管理工具的复杂React应用程序。这样的工具很少。就个人而言,我MobX。不幸的是,Redux被认为是“行业标准”。



Redux是React核心创建者的创意。也就是说,他们首先创建了一个很棒的React库。但是他们立即意识到,用她的手段来管理国家几乎是不可能的。如果他们没有找到解决这个(否则很棒)库固有问题的方法,我们许多人将永远不会听说过React。



因此,他们提出了Redux。

如果React是Mona Lisa,那么Redux就是胡子。如果使用Redux,则几乎必须在每个项目文件中编写大量样板代码。故障排除和阅读代码变得地狱。业务逻辑被带到后院。该代码包含混乱和动摇。



但是,如果开发人员可以选择:React + Redux或没有任何第三方状态管理工具的React,他们几乎总是选择React + Redux。由于Redux库是由React核心作者开发的,因此默认情况下它被认为是批准的解决方案。而且大多数开发人员更喜欢使用像这样被默认批准的解决方案。



当然,Redux将创建整个依赖项网络在您的React应用程序中。但是,公平地说,任何通用状态管理工具都将执行相同的操作。状态管理工具是变量和函数的共享存储库。此类功能和变量可由有权访问共享存储的任何组件使用。这有一个明显的缺点:所有组件都依赖于共享存储。



我认识的大多数React开发人员都曾尝试抵制使用Redux最终放弃了。 (因为……抵抗是没有用的。)我知道很多人立即讨厌Redux。但是,当他们面临选择-Redux或“我们会找到另一个React开发人员”的选择时,他们就投身已经同意将Redux视为他们生活中不可或缺的一部分。就像税收。就像直肠检查一样。喜欢去看牙医。



在React中反应共享价值



我太固执,不能轻易放弃。看了Redux之后,我意识到我需要寻找其他解决方案。可以使用Redux。我曾在使用此库的团队中工作。总的来说,我了解她的所作所为。但这并不意味着我喜欢Redux。

就像我之前说的,如果没有一个单独的状态管理工具,MobX大约要比Redux好一百万倍!但是我被一个更严重的问题折磨了这触动了React开发人员的集体思想:



为什么我们总是总是首先使用状态管理工具?



当我第一次开始使用React进行开发时,我花了很多晚上寻找替代解决方案。而且我发现了许多React开发人员都忽略的方法,但是没人能说出原因会解释。



想象一下,在我上面写过的假设应用程序中,我们创建了一个像这样的文件:



// components.js
let components = {};
export default components;


就这样。只有两行短代码。我们创建一个空对象-好的旧JS对象默认情况下,我们使用导出export default



现在让我们看一下组件内部的代码是什么样的<ContentArea>



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      components.ContentArea = this;
   }

   consoleLog(value) {
      console.log(value);
   }

   render() {
      return <MainContentArea/>;
   }
}


在大多数情况下,它看起来像一个完全正常的基于类的React组件。我们有一个简单的函数render(),可以访问树中的下一个组件。我们有一个小的函数console.log(),可以将代码执行的结果打印到控制台,以及一个构造函数。但是...构造函数中有些细微差别



从一开始,我们就导入了一个简单的对象components。然后,在构造函数中,向对象添加一个新属性components,其名称为当前React组件(this的名称this。现在,每次访问components对象时,我们都可以直接访问component <ContentArea>



让我们看看在层次结构底部发生了什么。该组件<TicketDetail>可以是这样的:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      components.ContentArea.consoleLog('it works');
      return <div>Here are the ticket details.</div>;
   }
}


这是发生了什么。每次渲染组件时,TicketDetail都会调用consoleLog()存储在组件中的函数ContentArea



请注意,函数consoleLog()不会通过prop传递到整个层次结构。实际上,该功能consoleLog()不会传递到任何组件,而是传递给任何组件。



但是它TicketDetail可以调用consoleLog()存储在中的函数ContentArea,因为我们做了两件事:



  1. ContentArea加载后,组件将指向其自身的链接添加到组件共享对象。
  2. TicketDetail加载时,组件导入了一个共享库components,也就是说,尽管属性没有通过prop传递给组件,但它可以直接访问该组件ContentAreaContentAreaTicketDetail


这种方法不仅适用于函数/回调。它可以用来直接查询状态变量的值。让我们想象一下这样的<ContentArea>样子:



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   render() {
      return <MainContentArea/>;
   }
}


然后我们可以这样写<TicketDetail>



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return <div>Here are the ticket details.</div>;
   }
}


现在,每一个组件渲染的时候<TicketDetail,它会寻找变量的值state.reduxSucks<ContentArea>。如果变量返回一个值true,则该函数console.log()将向控制台输出一条消息。即使变量的值ContentArea.state.reduxSucks从未通过props向下传递到树(传递给任何组件),也会发生这种情况。因此,有了一个位于标准React生命周期之外的简单底层JS对象,我们可以做到这一点,以便任何后代都可以直接从加载到component对象中的任何父级读取状态变量。我们甚至可以在其后代中调用父组件的函数。



直接在子组件中调用函数的能力意味着我们可以直接从其子组件更改父组件的状态。例如这样。



首先,在组件中,<ContentArea>我们将创建一个简单的函数来更改变量的值reduxSucks



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
   }

   render() {
      return <MainContentArea/>;
   }
}


然后,在组件中,<TicketDetail>我们将通过对象调用此方法components



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}


现在,在每次渲染组件之后,即使函数从未通过props传递到树上<TicketDetail>用户也可以按下将ContentArea.state.reduxSucks实时更改(切换)变量值的按钮ContentArea.toggleReduxSucks()



通过这种方法,父组件可以直接从其子组件调用该函数。这是操作方法。更新后的组件<ContentArea>将如下所示:



// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
   }

   render() {
      return <MainContentArea/>;
   }
}


现在让我们向组件添加逻辑<TicketTable>像这样:



// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';

export default class TicketTable extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucksHasBeenToggledXTimes: 0 };
      components.TicketTable = this;
   }

   incrementReduxSucksHasBeenToggledXTimes() {
      this.setState((previousState, props) => {
         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
      });      
   }

   render() {
      const {reduxSucksHasBeenToggledXTimes} = this.state;
      return (
         <>
            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
            <TicketRow data={dataForTicket1}/>
            <TicketRow data={dataForTicket2}/>
            <TicketRow data={dataForTicket3}/>
         </>
      );
   }
}


结果,组件<TicketDetail>未更改。它仍然看起来像这样:



// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}


您是否注意到这三类的奇特之处?在应用程序的层次结构中ContentArea,这是的父组件TicketTable,这是的父组件TicketDetail这意味着当我们安装一个组件时ContentArea,它还没有“知道”它的存在TicketTable,而toggleReduxSucks()ContentArea隐式编写的函数会调用子函数:

incrementReduxSucksHasBeenToggledXTimes()事实证明代码将无法正常工作,对吧?



但不是。



看。我们已经在应用程序中创建了多个级别,只有一种方法可以调用函数toggleReduxSucks()像这样。



  1. 我们安装并渲染ContentArea
  2. 在此过程中,将对组件的引用加载到components对象中ContentArea
  3. 结果被安装并渲染TicketTable
  4. 在此过程中,将对组件的引用加载到components对象中TicketTable
  5. 结果被安装并渲染TicketDetail
  6. « reduxSucks» (Toggle reduxSucks).
  7. « reduxSucks».
  8. toggleReduxSucks(), ContentArea.
  9. incrementReduxSucksHasBeenToggledXTimes() TicketTable .
  10. , , « reduxSucks», TicketTable components. toggleReduxSucks() ContentArea incrementReduxSucksHasBeenToggledXTimes(), TicketTable, components.


事实证明,我们的应用程序的层次结构允许我们向组件添加ContentArea算法,该算法将从子组件中调用函数,尽管事实上,当组件ContentArea安装时该组件并不知道组件的存在TicketTable



财富管理工具-转储



正如我所解释的,我深信ReduxMobX的不二之选。而且,当我有幸从头开始进行项目工作时(不幸的是,并不经常),我总是为MobX竞选。不适合Redux。但是,当我开发自己的应用程序时,我很少使用第三方状态管理工具-几乎从未使用过相反,我只是尽可能地缓存对象/组件。如果这种方法不起作用,我通常会退回到React的默认解决方案中,也就是说,我只是通过prop传递函数/状态变量。



这种方法的已知“问题”



我很清楚,我缓存基础对象的想法components并不总是适合解决共享状态/函数问题。有时候这种方式可以... ...开玩笑否则可能根本无法工作这里要记住一点。



  • 最适合单身人士



    例如,在我们的层次结构中,<TicketTable>组件包含具有零对多关系的<TicketRow>组件。如果要在组件缓存中将对<TicketRow>组件内的每个潜在组件(及其子元素<TicketDetail>)的引用缓存在组件缓存中,则必须将它们存储在数组中,这可能会很棘手。我一直都避免这种情况。
  • components , / , components. .

    , . , , . / , , components.
  • , components, ( setState()), setState(), .




现在,我已经解释了我的方法及其局限性,我必须警告您。自从我发现这种方法以来,我一直与认为自己是专业的React开发人员的人们分享它。每次他们回答相同的事情:



嗯...不要那样做。他们皱着眉头,表现得像我刚弄乱了空气。在他们看来我的方法有点...错误。同时,根据他们丰富的实践经验,还没有人向我解释到底是什么错。只是每个人都认为我的方法...亵渎神灵



因此,即使您喜欢这种方法或在某些情况下觉得方便,我也不建议如果您想获得React开发人员的工作,请在面试中谈论它。我认为,即使只是其他React开发人员交谈,您也需要思考一百万次才能谈论这种方法,或者最好不要说任何话。



我发现JS开发人员(尤其是React开发人员)可能过于笼统有时他们确实解释了为什么方法A是“错误”而方法B是“正确”的原因。但是在大多数情况下,即使他们自己无法解释原因,他们也只是看一段代码并将其声明为“错误”。



那么为什么这种方法对React开发人员如此烦人呢?



正如我所说,我的同事们都无法合理地回答为什么我的方法不好。如果有人愿意用答案来答谢我,那通常是以下借口之一(其中很少)。



  • , .



    .... , , Redux ( MobX, ) / React-. , . — . , /, . : , components. , / components, / , components. /, components , components . , , . , , Redux, MobX, - .
  • React « ». … .



    … . ? , . — - « » « », , , . React, . , . . « », . , React 100 %, ( ) , .


, ?



我写这篇文章是因为多年来(在个人项目中)一直使用这种方法。而且效果很好。但是,每次我走出个人泡沫,尝试与其他第三方React开发人员就此方法进行明智的对话时,我只会遇到对“行业标准”的明确表述和愚蠢的判断。



这种方法真的好吗?好吧,真的。我想知道。如果这确实是“反模式”,我将非常感谢那些证明其不正确性的人。答案“我不习惯这个”不适合我。不,我并不痴迷于这种方法。我并不是说这是React开发人员的灵丹妙药。我承认这并不适用于所有情况。但是也许谁能告诉我这是怎么回事?



我真的很想知道您对这件事的看法-即使您将我吹牛逼。



免费课程:






All Articles