在各种项目的代码中,通常必须按时进行操作-例如,要将用户的应用程序逻辑与当前时间联系起来。 高级界面开发人员Victor -homyakov,Victor
Khomyakov,描述了在不同作者的Java,C#和JavaScript项目中遇到的典型错误。 他们面临着相同的任务:获取当前日期和时间,测量时间间隔或异步执行代码。

-在Yandex之前,我曾在其他食品公司工作。 这不像是自由职业者-我写,通过并忘记了。 一个代码库需要花费很长时间。 我实际上观看,阅读,编写了许多不同语言的代码,并且看到了很多有趣的东西。 结果,我诞生了这个故事的主题。
例如,我看到在使用不同语言的不同项目中,出现了相同或非常相似的任务-使用日期和时间。 除了此类工作本身之外,它还可以是代码中带有日期和时间对象的弹出操作。

事实证明,无论您是前端还是后端,使用异步代码都具有类似的任务。 如果您在后端,则这些是对数据库的查询,远程调用。 如果是前端-您自然会拥有AJAX。 不同项目中的不同人几乎以相同的方式解决这些问题,这是人的本质。 使用类似的任务,无论您使用哪种语言,您都将做出类似的决定。 逻辑上,您-我们,我-同时犯了非常类似的错误。
最后我想谈什么? 关于这些重复模式,无论您使用何种语言编写,都容易出错,以及如何避免出现这些重复模式。
第一部分实际上是时间。 如您所知,时间在流逝。 示例:您需要为过去整天的昨天写一份报告。 您向数据库发出请求,您需要获取日期大于或等于昨天且小于今天的所有记录。 也就是说,您从“今天减去一天”的日期开始,直到今天的日期,不包括它。

因此,通常来说,线性编写代码。 开始日期-今天减去一天,结束日期-今天。 似乎一切正常,但是恰好在午夜,您有一件奇怪的事。 您的开始日期在这里。 开始日期减去一天-事实证明是这样。 此后,由于某种原因,报告的结束日期完全不同。

您,或者更确切地说,您的老板,得到的报告是两天而不是一天。 技术经理和经理会过来抱怨,并礼貌地建议您在六个月内转到另一个团队。

但是,随后您便有了新知识。 您知道时间不会停滞不前。 也就是说,两次调用Date.now()或获取新的Date(),您不希望获得相同的值。 有时可能相同,但可能不相同。 因此,如果您使用一种方法,任何一种逻辑,那么很可能只应该调用一次Date.now()或获取新的Date()(当前时间点)。
或者让我们走到另一边:在数据处理流中,所有与含义相关的值-报告的开头和结尾-必须严格从一个对象中计算出来。 例如,不是来自两个类似的对象,而是一个。 您将获得这些新知识,并迁移到新团队。 在那里,人们更加关注代码的速度和性能。

并且为您提供了将代码与日志重叠的功能,以衡量操作花费的时间。 如果这是一个困难的操作,则重要的是不要降低客户端的速度。 如果您在后端的Node上写一些东西,这也是一件困难的事情,那么他们会问您:“请在日志中写多长时间,然后我们将根据用户代理来计算客户的行为。”
然后,两个已经很新的老板来找您,并在日志中向您显示一个条目,您突然在其中记录了负面的时间。 而且,他们还礼貌地为您提供了在六个月内转到另一个团队的机会。

您将获得宝贵的知识,实际上,这些信息是获取日期,使用时间的方法-它们只是显示操作系统时钟中的内容。 他们也不保证统一的变更。 也就是说,在您的实时时间里,您的Date.now()可以同时跳一秒,并且可以跳得更多-少了一点。 原则上,它们通常不保证变更的单调性。 即,如本例所示,它可能会突然减小,Date.now()的值可能会突然减小。
是什么原因 及时同步。 在类似Linux的系统上,有诸如NTP守护程序之类的东西,它可以将操作系统的时钟与Internet上的确切时钟同步。 而且如果您有任何滞后或领先,它可能会人为地减慢或加快您的手表的速度,或者如果您有很大的时间间隔,他会明白,他将无法以不起眼的步伐,仅凭一次跳跃就可以抓住正确的时间改变它。 结果,您在手表的读数上会出现差距。
或者,您可能会使其更加复杂:对时钟有控制权的用户本人,他可能还想更改时钟。 他真的很想。 而且我们无权阻止他。 在日志中,我们会休息一下。 并且,因此,已经存在对该问题的解决方案。 很简单:有时间供应商。 如果您使用的是浏览器,则为performance.now(),如果您使用Node编写,则有一个高分辨率计时器,这两个计时器均具有均匀性和单调性。 也就是说,这些时间戳供应商总是只会增加,同时平均在一秒钟内平均增加。

后端有同样的问题。 你写什么语言都没关系。 例如,您可以搜索单调一致的手表,而问题将为您提供几乎所有语言的显示。 Rust中存在相同的问题。 使用Python,Java和其他语言的程序员也会感到痛苦。 在这些语言中,人们也踩了耙,这个问题是众所周知的,有解决方案。 例如,对于Java,有一个调用具有统一性和单调性的相同属性。
例如,如果您有一个分布式系统,例如时髦的微服务,那么它会更加复杂。 在N台不同的机器上有N种不同的服务,通常,原则上时钟永远不会收敛到一个指示,甚至没有希望。
而且,如果您在记录操作时遇到问题,则可以只记录时间的向量。 事实证明,您从涉及处理一个请求的N个系统中记录了N次。 或者,您只需转到抽象计数器,该计数器会简单地增加:1、2、3、4、5,通过此操作,它只会在每台计算机上平均滴答。 然后编写此类计数器,以链接在不同计算机上处理所有请求的所有这些阶段,并了解何时,何时发生,以什么顺序进行。
同样不要忘记:如果您是前端或后端,并且与前端紧密连接,那么我们的前端加后端也是分布式系统。 而且,如果您也对客户工作中的某些困难环节感兴趣,请首先尝试不要混淆,当您查看日志时,您会看到什么时间:“这是该操作多次发生的记录“-您看到服务器时间还是客户端时间?” 其次,尝试收集两次时间,因为正如我所说,时间可以朝着不同的方向发展。
时间足够了。 第二部分更加不稳定。

这是一个例子。 当用户不确切知道他想要什么时,会有一个非常有用的界面元素。 这称为建议或自动完成。 我们可以告诉他选择继续申请的选项。 也就是说,对于用户而言,这是一个很大的好处。 当我们立即告诉他我们知道我们可以进一步招聘什么时,对他来说工作更加方便。
但是,不幸的是,如果我们的网络速度稍慢,或者提供答案和继续选项的后端速度变慢,那么我们可以获得如此有趣的效果。 用户键入,键入,然后给出正确答案,我们看到了,然后一切都中断了。 由于某种原因,我们根本看不到想要看到的东西。 在这里,我们看到了正确的答案,并立即对某种中间状态有些废话。 同样,纯粹的痛苦和折磨。 我们的老板来找我们,要求我们修复此错误。

我们开始理解。 我们得到什么? 当用户键入文本时,我们将生成顺序异步请求。 也就是说,他设法输入的内容将发送到后端。 他进一步拨号,我们向后端发送了第二个请求,没有人保证我们的回调将以完全相同的顺序被调用。

这些是可能的查询和回调选项。 最明显的是,当我们写时,我们认为:他们发送了第一个请求,收到了第一个响应,发送了第二个请求,收到了答案。 如果用户输入速度非常快,那么我们可以考虑第二个选项,即我们设法发送了第一个请求,用户设法在输入第一个答案之前输入了一些内容。 然后是第一个答案,第二个答案。 这就是我们在视频中看到的内容,当建议无法正常使用时,这是第三个选项,通常会被人们遗忘,因为通常没有人保证答案的顺序。

在前端供应商中,如果您正在开发接口,则此问题非常普遍。 特别是,我们刚刚看到的带有“自动完成”的“ example”示例。 也就是说,有一个请求流,并且有一个异步到达的响应流。
如果有选项卡。 举起你的手,在GitHub上谁曾经至少提出过一次拉取请求? 您还记得,事实上,选项卡式界面是基于的,即,有一个选项卡,其中包含一系列注释,有一个带有提交的选项卡,还有一个包含代码本身的选项卡。 这是一个选项卡式界面。 而且,如果您切换到相邻的选项卡,则其内容将首次异步加载。
如果快速单击不同的选项卡,可能会发现您已打开它们,然后看到内容的加载闪烁。 最后,您不会看到正确选项卡的内容,如果您正确的话,当然不要自己写。
例如,如果您有一家商店,则可以将商品快速拖到购物篮中。 一些快速,敏锐的用户拖了十个商品,然后他看到价格如何闪烁,相对而言,是100卢布,10卢布,50卢布,75卢布,然后停在1卢布。 他不相信你,他认为你写得不好,想欺骗他,就离开商店不买任何东西。
一个例子。 如果您有某种Scrum或看板或其他东西,并且使用电子板拖放卡,则在将它们拖到错误的列中时,您可能至少错过了一次。 这发生了吗? 当然,您会抓紧自己并立即抓住它,然后将其拖动到应有的位置。 在这种情况下,您很快就会生成两个查询。 在不同的系统中,此后会出现错误。 您将其拖到正确的列中-第一个请求的答案已到达,卡再次跳到您将其转移到的列中。 结果很丑。

道德是什么? 假设您具有相同类型的请求源。 然后,如果可能,如果下一个请求到达,则中断所有未完成的请求,以免浪费资源,以便后端知道-您不再需要它。
因此,在处理响应时,您还可以控制所有内容。 而且,如果响应到达了您不需要的较早请求,您也将显式忽略它。

因此,问题已经存在了很长时间,并且解决方案也已经存在。 例如,在RxJS库中。 这直接是来自文档的一个示例,正确的Hello world,如何编写正确的自动完成功能。 开箱即用,就无视较旧的错误请求的答案了。

如果您在Redux和Redux-Saga上进行编写,那么通常也是如此,并且所有内容也都写在文档中。 但是它被深深地埋了,显然没有说这是一个错误,我们就这样修复它。 只是一个描述。
由于我们已经进入了React,因此我们将更近一步。

这是我们存储库中的一部分真实代码。 有人和我们一起刷卡。 而且,请您在获取地图时,建议在用户所在的位置上显示一个标记。 但这都是在浏览器中发生的。 也就是说,如果您启用了地理位置,那么我们可以获得您的坐标,并且可以直接指示您在地图上的位置。
如果不允许地理定位,或者如果在那里发生了某种错误,那么建议我们显示带有错误的骰子。 就是说,这里我们显示了骰子,我们无法显示你在哪里,伙计,三秒钟后,我们将其删除,这是骰子。 您可能设法阅读了。 此外,移动的物体(如可伸缩的模具和消失的物体)立即引起注意,并且您会立即注意到并阅读它。
但是,如果您仔细看一下这段代码中发生的情况,那么我们会在三秒钟后更改组件的状态。 在这三秒钟内什么都可能发生。 包括用户可以关闭此卡很长时间,并且您的组件将被卸载,清理其状态。

因此,您将自己朝腿开枪,然后朝弹道射击,这将在三秒钟后结束。 那应该怎么办? 不要忘记,如果您执行此类挂起的操作,则可以通过卸载正确地清理它们。 在具有其他生命周期方法的其他框架中,这也是合乎逻辑的。 当您进行某种破坏,销毁或其他卸载操作时,您必须正确记住要清理这些东西。

您的代码在浏览器中的何处可以被推迟? 会有油门和反跳之类的事情。 他们在后台已经设置了setTimeout,setInterval,这些我已经介绍过了。 仍然有requestAnimationFrame,仍然有requestIdleCallback。 还有AJAX请求-AJAX请求回调可以称为延迟。 也不要忘记它们,它们也需要清洗。

而且,如果我们进一步深入研究,我们将了解到,最初,整个问题都被抽象化为具有某种生命周期的某种要素,因此我们推迟了电话。 我们在一个寿命长的对象内部创建,该对象的寿命比原始对象更长。 也就是说,有两个对象的生命周期不匹配,而生命周期却不匹配。 并从这两个错误立即流失。
首先是我们现在所拥有的:一个长寿的对象持有一个指向您函数的链接并调用它,尽管您已经死亡。 第二个是相关内存的泄漏。 也就是说,一个寿命很长的对象拥有一个指向您代码的链接,并且不允许对其进行清理,从内存中收集。
第三部分与第二部分相反。 相反,她是关于同步的。

像往常一样,存在着一连串的承诺-然后是,然后是那里。 而且在这段代码中,如果您看上去很清楚,编写得很整洁,是否是支持者,或者至少听说过有关功能方法,纯函数,没有副作用的内容,那么您可以理解,可以在此代码中完成某些工作加快速度。
由于这两个请求是异步的,因此它们显然彼此独立。 如果不确定这一点,则意味着您写错了什么,也就是说,您显然有某种副作用,全局状态等。 如果您写得好,那么对您而言立即变得显而易见。 顺便说一句,这里是从功能的纯净度,从没有副作用而获得的明显收益。 因为在这里,阅读此代码时,您了解它们可以并行化。 它们彼此独立。 而且,通常,它们甚至有可能被交换。

这样做是这样的。 我们并行运行两个查询,等待它们完成,然后执行以下代码。 也就是说,获利何在? 首先,我们的代码运行速度更快,因此,我们不必等待一个请求启动第二个请求。 而且我们会跌得更快。 如果第二个请求中有错误,那么我们将不会浪费时间等待第一个请求被执行以立即落在第二个请求上。

为了完整起见,Promise API还有什么呢? 这是Promise.all(),它并行运行所有请求,并等待执行。 有Promise.race(),它正在等待第一个成功。 而且,通常,标准API中没有其他内容。

我们已经知道,如果有问题,那么有人已经为我们解决了。 有一个异步库,其中有很多用于管理异步任务的库。 有一些用于并行运行异步任务的方法。 有一些方法可以依次运行。 有一些用于组织异步迭代器的方法。 也就是说,您知道有一个可以在其中运行forEach()的数组。 但是,如果您需要在forEach()中调用异步函数,那么您要么立即遇到问题,然后拒绝forEach()自己编写一些东西,要么使用现成的库准备使用相同的异步对象。 您知道,使用某种异步的迭代器来调用map(),再调用forEach()-它已经在框中了。

另一个选择是蓝鸟库。 他们称之为正确的Promise.any()。 , , : N , N - , , . , , . .
Promise.race(), , promise , , , . . Promise.any() — reject. . reject , resolve , , . . promise — , .
, map, reduce, each, filter . API , Async JS, . promise . , , , promise. .
promise? , async/await.

. . . ,
«» . , webdriver. , , - , . . . webdriver.
, await. . , - . await, — , , ! .

— Promise.all(). , await.

: await , then . , .
, . : await, , — , .
, , :
, -, :
? , — Lodash, RxJS . . , . , - . . — , , . .