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:
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:
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: ondata
Assim, onerror
Assim, onfinish
. Fácil de peasy.
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.
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.
Em seguida, convertemos nossas duas listas em fluxos legíveis, usando métodos diferentes para cada tipo de entrada (uma matriz, um arquivo etc.).
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.
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.
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.
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.
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 handleDiffChunk
o contexto de outputDiffChunk
é preservado, e é por isso que podemos acessar o tampão Diff cada vez que processamos um novo objeto Diff.
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!
Links
Repositório | Documentação | Npm