No código de vários projetos, muitas vezes é preciso operar dentro do prazo - por exemplo, para vincular a lógica do usuário do aplicativo ao horário atual. Victor Khomyakov, Victor
-homyakov, desenvolvedor sênior de interface, descreveu os erros típicos que encontrou em projetos em Java, C # e JavaScript de vários autores. Eles enfrentaram as mesmas tarefas: obter a data e hora atuais, medir intervalos ou executar código de forma assíncrona.

- Antes da Yandex, trabalhei em outras empresas de alimentos. Isso não é como um freelancer - eu escrevi, passei e esqueci. Demora muito tempo para trabalhar com uma base de código. Na verdade, eu assisti, li, escrevi muito código em diferentes idiomas e vi muitas coisas interessantes. Como resultado, nasci o tema desta história.
Por exemplo, vi que em diferentes projetos em diferentes idiomas surgem tarefas iguais ou muito semelhantes - trabalhando com data, hora. Além desse trabalho, ele pode ser operações pop-up no código com objetos de data e hora.

Acontece que, independentemente de você ser o front-end ou back-end, você tem tarefas semelhantes para trabalhar com código assíncrono. Se você estiver no back-end, essas são consultas ao banco de dados, chamadas remotas. Se o front-end - você naturalmente tem AJAX. Pessoas diferentes em projetos diferentes resolvem esses problemas quase da mesma maneira, essa é a essência do homem. Com uma tarefa semelhante, você toma uma decisão semelhante, independentemente do idioma que pensa. E é lógico que, ao mesmo tempo, você - nós, eu - cometamos erros muito semelhantes.
Sobre o que eu quero falar no final? Sobre esses padrões repetidos que ocorrem independentemente do idioma em que você escreve, erros fáceis de cometer e como não fazê-los.
A primeira parte é dedicada, de fato, ao tempo. Como você sabe, o tempo se move. Exemplo: você precisa escrever um relatório para ontem, para o último dia inteiro. Você faz uma solicitação ao banco de dados, precisa obter todos os registros cuja data é maior ou igual a ontem e menor que hoje. Ou seja, você começa a partir da data "hoje menos um dia" e até a data de hoje, sem incluí-la.

De maneira linear, você, em geral, escreve o código. Data de início - hoje menos um dia, data de término - hoje. Parece que tudo funciona, mas exatamente à meia-noite você tem uma coisa estranha. A sua data de início está aqui. Data de início menos um dia - acontece isso. Depois disso, por algum motivo, a data final do relatório é completamente diferente.

Você, ou melhor, seu chefe, recebe um relatório por dois dias em vez de um. O gerente técnico e o gerente vêm, reclamam e oferecem educadamente que você mude para outra equipe em seis meses.

Mas então você é enriquecido com novos conhecimentos. Você entende que o tempo não pára. Ou seja, chamando Date.now () duas vezes ou obtém novo Date (), você não espera obter o mesmo valor. Às vezes pode ser o mesmo, mas pode não ser o mesmo. Portanto, se você tiver um método, qualquer parte da lógica, provavelmente haverá apenas uma chamada para Date.now () ou a obtenção de nova Date (), o ponto atual no tempo.
Ou vamos por outro lado: no fluxo de processamento de dados, todos os valores relacionados por significado - o início e o final do relatório - devem ser calculados estritamente a partir de um objeto. Não de dois similares, por exemplo, mas de exatamente um. Você é enriquecido com esse novo conhecimento, muda para uma nova equipe. Lá, as pessoas estão mais preocupadas com a velocidade e o desempenho do código.

E você pode sobrepor o código ao log, para medir quanto tempo leva uma operação. Se esta é uma operação difícil, é importante que não diminua a velocidade do cliente. Se você escrever algo no back-end, no Node, também é uma transação difícil, e eles perguntarão: "Escreva no log quanto tempo leva e calcularemos como nossos clientes se comportam dependendo do agente do usuário".
Em seguida, dois novos chefes chegam até você e mostram uma entrada no log, onde você repentinamente registra tempos negativos. E eles também oferecem educadamente que você mude para outra equipe em seis meses.

Você ganha um conhecimento valioso de que, de fato, os métodos para obter a data e a hora que você usa - eles apenas mostram o que você tem no relógio do sistema operacional. Nem garantem mudanças uniformes. Ou seja, no seu segundo tempo real, o seu Date.now () pode pular por um segundo e um pouco mais - um pouco menos. E, em princípio, eles geralmente não garantem a monotonia da mudança. Ou seja, como neste exemplo, ele pode diminuir repentinamente, o valor de Date.now () pode diminuir repentinamente.
Qual o motivo? Sincronização em tempo. Em sistemas semelhantes ao Linux, existe o daemon NTP, que sincroniza o relógio do seu sistema operacional com o relógio exato na Internet. E se você tiver algum atraso ou vantagem, ele pode desacelerar artificialmente ou acelerar o relógio, ou se você tiver um intervalo de tempo muito grande, ele entenderá que não conseguirá acertar o tempo certo com passos discretos e apenas com um salto. muda isso. Como resultado, você obtém uma lacuna nas leituras do seu relógio.
Ou você pode complicar bastante: o próprio usuário, que tem controle sobre o relógio, ele também pode querer apenas mudar o relógio. Ele realmente queria. E não temos o direito de detê-lo. E nos logs, temos pausas. E, consequentemente, uma solução para esse problema também já existe. É simples: existem fornecedores de tempo. Se você estiver em um navegador, então é performance.now (), se você estiver escrevendo no Node, haverá um timer de alta resolução, que, ambos, têm essas propriedades de uniformidade e monotonia. Ou seja, esses fornecedores de carimbos de data / hora, eles sempre aumentam e, ao mesmo tempo, de maneira uniforme em um segundo do tempo real.

O back-end tem o mesmo problema. Não importa qual idioma você escreve. Por exemplo, você pode procurar por um relógio consistente e monótono, e o problema é o mesmo, no qual quase todos os idiomas são apresentados. Existe o mesmo problema no Rust. Há também a dor de um programador que está em Python, em Java e em outras linguagens. Nessas línguas, as pessoas também pisaram em um ancinho, esse problema é conhecido, existe uma solução. Por exemplo, para Java, há uma chamada que tem as mesmas propriedades de uniformidade e monotonia.
Se você tem um sistema distribuído, microsserviços modernos, por exemplo, ainda é mais complicado. Existem N serviços diferentes em N máquinas diferentes, o relógio no qual, em geral, nunca pode convergir para uma indicação em princípio; não há nada que esperar.
E se você tiver um problema ao registrar ações, poderá registrar apenas um vetor de tempo. Acontece que você registra N vezes de N sistemas envolvidos no processamento de uma solicitação. Ou basta ir ao contador de abstratos, que simplesmente aumenta: 1, 2, 3, 4, 5, ele apenas funciona de maneira uniforme em cada máquina com esta operação. E você escreve esses contadores para vincular todos esses estágios de processamento de suas solicitações em máquinas diferentes e para entender melhor quando, o que acontece, em que sequência.
Também não se esqueça: se você é front-end ou back-end, que trabalha com o front-end em estreita conexão, nosso front-end mais back-end também é um sistema distribuído. E se você também estiver interessado em algum tipo de sessão difícil do trabalho do cliente, tente também, primeiro, não confundir, quando você olhar nos logs, a que horas você vê: “aqui está o registro de que essa operação ocorreu tantas vezes "- você vê a hora do servidor ou a hora do cliente?" E, em segundo lugar, tente coletar os dois momentos, porque, como eu disse, os tempos podem ir em direções diferentes.
Chega de tempo. A segunda parte é mais irregular.

Aqui está um exemplo. Existe um elemento de interface muito útil quando o usuário não sabe exatamente o que deseja. Isso é chamado de sugestão ou preenchimento automático. Podemos dizer a ele opções para continuar a solicitação. Ou seja, para o usuário, esse é um benefício muito grande. É muito mais conveniente para ele trabalhar quando lhe mostramos imediatamente que sabemos o que podemos recrutar ainda mais.
Mas, infelizmente, se tivermos uma rede um pouco lenta ou se o back-end, que fornece respostas, opções de continuação, diminuir, podemos obter efeitos tão interessantes. O usuário digita, digita, então a resposta correta vem, nós a vemos, e então tudo quebra. Por alguma razão, não estamos vendo o que queríamos ver. Aqui vemos a resposta correta e imediatamente algumas bobagens para algum tipo de estado intermediário. Mais uma vez, pura dor e sofrimento. Nossos chefes vêm até nós e nos pedem para corrigir esse bug.

Começamos a entender. O que nós ganhamos? Quando o usuário digita seu texto, obtemos a geração de solicitações assíncronas sequenciais. Ou seja, o que ele conseguiu digitar, enviamos para o back-end. Ele disca ainda mais, enviamos uma segunda solicitação para o back-end e ninguém nunca nos garantiu que nossos retornos de chamada serão chamados exatamente na mesma sequência.

Essas são as possíveis opções de consulta e retorno de chamada. O mais óbvio, quando escrevemos, pensamos: eles enviaram a primeira solicitação, receberam a primeira resposta, enviaram a segunda solicitação, receberam a resposta. Se o usuário digitar muito rapidamente, podemos pensar na segunda opção que conseguimos enviar a primeira solicitação, o usuário conseguiu digitar algo antes de receber a primeira resposta. Então veio a primeira resposta, a segunda resposta. E aqui está o que vimos no vídeo: quando a sugestão não funcionou corretamente, esta é a terceira opção, que muitas vezes é esquecida, de que ninguém garante a ordem das respostas, em geral.

E nos fornecedores front-end, esse problema é muito comum se você estiver desenvolvendo interfaces. Em particular, o exemplo com sugere, com preenchimento automático, que acabamos de ver. Ou seja, há um fluxo de solicitações e um fluxo de respostas chegando de forma assíncrona.
Se você tiver guias. Levante as mãos, quem no GitHub fez pelo menos uma solicitação de recebimento? Você lembra que, aliás, a interface com guias é baseada, ou seja, há uma guia onde há uma sequência de comentários, há uma guia com confirmações e há uma guia com o próprio código. Essa é uma interface com guias. E se você alternar para uma guia vizinha, seu conteúdo será carregado de forma assíncrona pela primeira vez.
Se você clicar rapidamente em guias diferentes, pode ser que você as tenha ativado e você verá o carregamento do conteúdo piscando. E, no final, não é fato que você verá o conteúdo da guia correta; se estiver correto, é claro, não escreva o seu.
Por exemplo, se você tiver uma loja, se arrastar rapidamente as mercadorias para a cesta. Um usuário rápido e perspicaz arrastou dez mercadorias e, em seguida, vê como seu preço está piscando e, relativamente falando, 100 rublos, 10 rublos, 50 rublos, 75 rublos e para em um rublo. Ele não acredita em você, pensa que você escreve mal, quer enganá-lo e sai de sua loja sem comprar nada.
Um exemplo Se você tem algum tipo de scrum ou kanban ou qualquer outra coisa e usa placas eletrônicas para arrastar e soltar cartões, provavelmente perdeu os cartões pelo menos uma vez quando os arrastou e soltou-os na coluna errada. Isso aconteceu? Obviamente, você se segura, agarra-o imediatamente e arrasta-o para onde deveria estar. Nesse caso, você gera rapidamente duas consultas. E em sistemas diferentes, existem bugs que surgem logo depois disso. Você arrastou-o para a coluna correta - a resposta para a primeira solicitação chega e o cartão novamente salta para a coluna onde você o transferiu. Acontece muito feio.

Qual é a moral? Suponha que você tenha uma fonte do mesmo tipo de solicitação. Se possível, se a próxima solicitação chegar, interrompa todas as solicitações incompletas para não desperdiçar recursos, para que o back-end saiba - você não precisa mais disso.
Assim, ao processar respostas, você também controla tudo. E se uma resposta chegar a uma solicitação anterior da qual você não precisa, você também a ignorará explicitamente.

Consequentemente, o problema existe há muito tempo e a solução também já existe. Por exemplo, na biblioteca RxJS. Este é diretamente um exemplo da documentação, Olá, mundo, como escrever o preenchimento automático correto. Logo que sai da caixa, há um desrespeito às respostas a solicitações incorretas mais antigas.

Se você escreve no Redux e no Redux-Saga, também existe, em geral, e tudo também está escrito na documentação. Mas lá está profundamente enterrado, e claramente não se diz que é um bug e nós o corrigimos assim. Apenas uma descrição é.
Como passamos ao React, vamos nos aproximar dele.

Este é um pedaço de código real que tínhamos em nosso repositório. Alguém desenha cartas conosco. E, por favor, quando você obtiver um mapa, é muito recomendável mostrar uma marca no local onde o usuário está localizado. Mas tudo isso está acontecendo no navegador. Ou seja, se você tiver a geolocalização ativada, poderemos obter suas coordenadas e indicar diretamente onde você está no mapa.
Se a geolocalização não for permitida ou se ocorrer algum tipo de erro, é aconselhável mostrar algum tipo de dado com um erro. Ou seja, aqui mostramos o dado, que não conseguimos mostrar onde você está, cara, e depois de três segundos o removemos, esse dado. Você conseguiu ler, provavelmente. Além disso, um objeto em movimento, como um dado retrátil e desaparecendo, atrai imediatamente a atenção e você notará imediatamente, lerá.
Mas se você observar cuidadosamente o que acontece nesse código, alteramos o estado do nosso componente após três segundos no tempo. Tudo pode acontecer nesses três segundos. Se o usuário puder fechar este cartão por um longo período de tempo e seu componente for desmontado, limpe seu estado.

Assim, você atira na perna e atira em uma trajetória balística, que terminará em três segundos. E o que deve ser feito? Não esqueça que, se você fizer essas operações pendentes, poderá limpá-las corretamente com desmontagem. E em outras estruturas com outros métodos de ciclo de vida, o mesmo é lógico. Quando você tem algum tipo de destruição, destruição, outra coisa, desmonte, você deve se lembrar corretamente de limpar essas coisas.

De onde no navegador seu código pode ser adiado? Existem coisas como acelerador e debounce. Eles setTimeout, setInterval, algo que eu já mostrei. Ainda existe requestAnimationFrame, ainda há requestIdleCallback. E solicitações AJAX também - os retornos de chamada de solicitação AJAX podem ser chamados adiados. Não se esqueça deles também, eles também precisam ser limpos.

E se mergulharmos ainda mais um nível, entenderemos que inicialmente todo o problema é abstraído para que tenhamos algum tipo de componente com algum tipo de ciclo de vida e adiamos a chamada. Criamos dentro de um objeto de vida longa, que tem uma vida útil mais longa que a original. Ou seja, existem dois objetos com um ciclo de vida incompatível, com uma vida útil incompatível. E a partir disso dois bugs fluem imediatamente.
O primeiro é o que temos agora: um objeto de longa duração mantém um link para sua função e o chama, embora você já tenha morrido. E o segundo é o vazamento de memória associada. Ou seja, novamente, um objeto de longa duração mantém um link para o seu código e não permite que ele seja limpo e coletado da memória.
A terceira parte é o oposto da segunda. Ela, pelo contrário, é sobre sincronização.

Há, como sempre, uma cadeia de promessas - então, então, então algo lá. E neste código, se você olhar, se escrever de maneira correta, se for um apoiador ou tiver ouvido pelo menos sobre a abordagem funcional, sobre funções puras, sobre a ausência de efeitos colaterais, poderá entender que algo pode ser feito nesse código acelerar.
Como essas duas solicitações são assíncronas, elas são claramente independentes uma da outra. Se você não tiver certeza disso, significa que você está escrevendo algo errado, ou seja, aí obviamente você tem algum tipo de efeito colateral, um estado global e assim por diante. Se você escreve bem, isso imediatamente se torna aparente para você. A propósito, aqui está um claro lucro da pureza da função, da falta de efeitos colaterais. Porque aqui, ao ler este código, você entende que eles podem ser paralelos. Eles são independentes um do outro. E, em geral, eles podem até ser trocados, provavelmente.

Isso é feito assim. Executamos duas consultas em paralelo, esperamos que elas terminem e, em seguida, executamos o código a seguir. Ou seja, lucro em quê? No fato de que, primeiramente, nosso código roda mais rápido, não esperamos que uma solicitação inicie a segunda. E nós cairemos mais rápido. Se houver um erro na segunda solicitação, não perderemos tempo aguardando a execução da primeira solicitação para cair imediatamente na segunda.

Para ser completo, o que mais temos na API Promise? Aqui está Promise.all (), que executa todas as solicitações em paralelo e aguarda a execução. Há Promise.race (), que está aguardando o primeiro deles ter êxito. E, em geral, não há mais nada na API padrão.

Já entendemos que, se houver um problema, alguém já o resolveu para nós. Existe uma biblioteca Async que possui uma seleção bastante rica para gerenciar tarefas assíncronas. Existem métodos para executar tarefas assíncronas em paralelo. Existem métodos para executar seqüencialmente um após o outro. Existem métodos para organizar iteradores assíncronos. Ou seja, você sabe que possui, por exemplo, uma matriz na qual pode executar forEach (). Mas se você precisar chamar uma função assíncrona em forEach (), você terá um problema imediatamente e recusará o forEach () e escreverá algo por conta própria ou usará uma biblioteca pronta para usar as mesmas coisas assíncronas. Você entende, chame map () com algum tipo de iterador assíncrono, chame forEach () - já está na caixa.

Outra alternativa é a biblioteca bluebird. Existe, como eles chamam, o Promise.any () correto. , , : 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 . . , . , - . . — , , . .