Início Notícias Você não pode comparar fluxos de dados maciços no JavaScript. Ou você...

Você não pode comparar fluxos de dados maciços no JavaScript. Ou você pode? 🤔

13
0

Eu sei, parece impossível. O JavaScript não é uma linguagem muito executada, principalmente por causa de seu modelo de thread único. Então, se eu lhe disser que vamos comparar 1,8 milhão de votos em uma eleição fictícia da cidade, diretamente no navegador, você provavelmente pensará que eu sou louco. Um navegador congelava e caiu – sempre. Adivinha? Você estaria certo. Aqui está o resultado com uma biblioteca de diff muito popular:

Demonstração de um pedido que tenta comparar 1,8 milhão de votos em duas eleições.Demonstração de um pedido que tenta comparar 1,8 milhão de votos em duas eleições.

Claramente, não é uau tempo. Mas com as técnicas corretas – gerenciamento de memória eficiente, uma arquitetura de streaming, trabalhadores da web e atualizações de lote – você pode fazê -lo. Aqui está a prova:

Demonstração de um pedido que compara suavemente 1,8 milhão de votos em duas eleições.Demonstração de um pedido que compara suavemente 1,8 milhão de votos em duas eleições.

Como você pode ver, mesmo com 1,8 milhões de objetos processados ​​em tempo real, a interface do usuário é perfeitamente responsiva. O mais importante, ao injetar pequenos lotes de dados ao longo do tempo, transformamos um espere por isso atraso em um Veja isso acontecer experiência.

Tudo bem, agora vamos mergulhar no processo de criação.

Desafios

Temos três desafios para resolver.

  • Alcançando o melhor desempenho.

  • Manuseio de diferentes formatos de entrada.

  • Sendo fácil para os desenvolvedores usarem.

Desempenho

A primeira coisa é evitar bloquear o fio principal a todo custo. Portanto, precisamos de código assíncrono, um trabalhador para lidar com o levantamento pesado e uma complexidade linear Sobre)o que significa que o tempo de execução cresce proporcionalmente com o tamanho da entrada. Além disso, o gerenciamento eficiente da memória é crucial.

O gerenciamento de memória eficiente requer liberação de memória assim que alguns dados forem processados ​​e também enviando progressivamente os dados em pedaços para minimizar as atualizações da UI. Por exemplo, em vez de enviar um milhão de diferenças de objeto seguidas, poderíamos enviar grupos de mil. Isso reduziria drasticamente o número de atualizações de DOM.

Formato de entrada

O segundo desafio é lidar com diferentes tipos de formatos de entrada. Precisamos ser versáteis. Devemos aceitar matrizes de objetos, arquivos JSON e até fluxos de dados. Portanto, antes de iniciar o processo DIF, precisamos converter tudo em um único formato: um fluxo legível.

Experiência do desenvolvedor

Finalmente, precisamos proporcionar uma ótima experiência de desenvolvedor. A função deve ser fácil de usar, fácil de personalizar. Basicamente, o usuário poderá fornecer duas listas à nossa função. Vamos compará -los e enviaremos de volta progressivamente o resultado.

A maneira mais simples de fazer isso é expor um ouvinte de eventos, com três tipos de eventos: ondataAssim, onerrorAssim, onfinish. Fácil de peasy.

Nossa função streamlistdiff e seus ouvintes de eventosNossa função streamlistdiff e seus ouvintes de eventos

O ondata o evento receberá uma variedade de diferenças de objeto, chamado de um chunk. O diferencial deve ser claro, com um valor anterior, um valor atual, um rastreador de índice e um status – igual, atualizado, movido ou excluído.

E como eu sei que a maioria de vocês ama o TypeScript, também teremos preenchimento automático. Correndo no bolo, os usuários poderão especificar opções para refinar a saída. Vamos ver como o algoritmo funciona.

Exemplo de uso com opções e digitaçãoExemplo de uso com opções e digitação

Passo a passo do algoritmo

Por uma simplicidade, o código pelo qual estamos passando vem do streamListDiff função do @donedeal0/superdiff biblioteca.

Vamos dar uma olhada na função principal. São necessárias duas listas para comparar, uma chave comum em todos os objetos, como um id Para exemplos, corresponder aos objetos entre as duas listas e algumas opções para refinar a saída.

No corpo da função, podemos ver que ele retorna um ouvinte de eventos antes de iniciar o diferencial. O truque é acionar o bloco lógico de forma assíncrona. Basicamente, o loop do evento executará todo o código síncrono primeiro e só então iniciará o trabalho real.

streamlistdiff: nossa principal funçãostreamlistdiff: nossa principal função

Em seguida, convertemos nossas duas listas em fluxos legíveis, usando métodos diferentes para cada tipo de entrada (uma matriz, um arquivo etc.).

Converter entradas de arquivo ou matriz em fluxos legíveisConverter entradas de arquivo ou matriz em fluxos legíveis

Uma vez que temos dois fluxos válidos, iteramos sobre os dois em paralelo graças a Promise.all(). Em cada iteração, fazemos duas coisas: primeiro, verificamos se o objeto é válido – se não, emitimos uma mensagem de erro. Segundo, verificamos se um objeto com uma propriedade de referência semelhante já está no buffer de dados da outra lista.

Iterando sobre os dois riachosIterando sobre os dois riachos

O que é um buffer de dados? Existem dois buffers, um para cada lista. A idéia é armazenar os objetos incomparáveis ​​em um hashmap para que a outra lista, que esteja sendo analisada no mesmo momento, possa fazer o check -in em tempo real se houver uma correspondência para seu objeto atual. Isso evita fazer duas iterações completas, sem resultados e alto consumo de memória, antes de iniciar o diferencial real. Não perdemos tempo e somos eficientes.

Usamos um hashmap para armazenar objetos incomparáveis, pois ele suporta qualquer tipo de valor como teclas, é muito executado e fornece um iterador pronta para uso.

Inserções simultâneas e recuperação de entradas correspondentesInserções simultâneas e recuperação de entradas correspondentes

Para encurtar a história, se houver uma correspondência no buffer, removemos imediatamente para liberar a memória e compará -la com o objeto atual. Depois de termos o diferencial, enviamos imediatamente ao usuário. Se não podemos fazer o comparoon, inserimos o objeto no buffer relevante, esperando para ser comparado. Posteriormente, simplesmente iteramos sobre cada buffer e processamos os objetos restantes da mesma maneira.

Enviando um diferencial para o usuárioEnviando um diferencial para o usuário

Agora, você provavelmente está se perguntando por que não fazemos uma única iteração na primeira lista e encontramos sua correspondência na segunda lista. Bem, se você tem um milhão de objetos, um find() A pesquisa será altamente ineficiente, pois levaria a um grande número de iterações. Mas o método do buffer de dados nos permite recuperar a correspondência com custo de desempenho zero graças ao has() método.

Map () é mais eficiente, dadas nossas restriçõesMap () é mais eficiente, dadas nossas restrições

Anteriormente, eu disse que enviamos imediatamente cada objeto diff para o usuário. Isso é parcialmente verdadeiro. Imagine que o usuário recebe um milhão de objetos em uma linha – ele pode sobrecarregar o thread principal. O que fazemos, em vez disso, é armazenar o objeto difere em outro buffer. Quando esse buffer atingir um determinado tamanho, digamos mil objetos, liberamos -o e enviamos um único lote de dados para o usuário.

Para fazer isso, usamos um fechamento. Vamos dar uma olhada no outputDiffChunk função. Ele tem uma matriz para armazenar as diferenças e retorna duas funções: handleDiffChunk e releaseLastChunks. handleDiffChunk Recebe um diferencial de objeto e o adiciona ao buffer se ainda não estiver cheio. Se estiver cheio, ele envia o lote para o usuário. Já que usamos uma única instância de handleDiffChunko contexto de outputDiffChunk é preservado, e é por isso que podemos acessar o tampão Diff cada vez que processamos um novo objeto Diff.

Em vez de enviar dados um de cada vez, enviamos -os em lotes.Em vez de enviar dados um de cada vez, enviamos -os em lotes.

Finalmente, releaseLastChunks é auto-explicativo. Depois que todos os diferenciais forem processados, ele libera o buffer Dif Dif uma última vez e envia os dados restantes para o usuário.

Em última análise, emitimos um finish evento, e isso é tudo.

Mais uma coisa

A demonstração que vimos anteriormente usa a lista virtualizada para renderizar grandes quantidades de itens no DOM e também aproveita requestAnimationFrame Para evitar atualizá -lo com muita frequência.

Como resultado, tudo funciona muito bem. Parece que John Doe foi eleito, parabéns a ele!

Repositório | Documentação | Npm

fonte