
Você já ouviu falar sobre memoization
? A propósito, é uma coisa super simples - apenas memorize qual resultado você obteve de uma primeira chamada de função e use-o em vez de chamá-lo pela segunda vez - não ligue para coisas reais sem razão, não perca seu tempo .
Ignorar algumas operações intensivas é uma técnica de otimização muito comum. Toda vez que você pode não fazer algo - não faça. Tente usar o cache - memcache
, file cache
, local cache
- qualquer cache! Um item essencial para os sistemas de back-end e uma parte crucial de qualquer sistema de back-end do passado e do presente.

Memoization vs Caching
Memoização é como cache. Um pouco diferente. Não cache, vamos chamá-lo de kashe.
Para encurtar a história, mas a memorização não é um cache, não é um cache persistente. Pode ser no lado do servidor, mas não pode e não deve ser um cache no lado do cliente. É mais sobre recursos disponíveis, padrões de uso e os motivos para usar.
Problema - O cache precisa de uma "chave de cache"
O cache está armazenando e buscando dados usando uma key
cache de sequência . Já é um problema construir uma chave única e utilizável, mas é necessário serializar e desserializar dados para armazenar novamente o meio baseado em string ... em suma - o cache pode não ser tão rápido quanto você imagina. Cache especialmente distribuído.
A memorização não precisa de nenhuma chave de cache
Ao mesmo tempo - nenhuma chave é necessária para memorização. Geralmente * ele usa argumentos como estão, não tentando criar uma única chave a partir deles, e não usa algum objeto compartilhado disponível globalmente para armazenar resultados, como o cache geralmente faz.
A diferença entre memorização e cache está na API INTERFACE !
Geralmente * não significa sempre. Lodash.memoize , por padrão, usa JSON.stringify
para converter argumentos passados em um cache de strings (existe alguma outra maneira? Não!). Só porque eles vão usar essa chave para acessar um objeto interno, mantendo um valor em cache. O memoize rápido , "a biblioteca de memoização mais rápida possível", faz o mesmo. Ambas as bibliotecas nomeadas não são bibliotecas de memorização, mas bibliotecas de cache.
Vale ressaltar - JSON.stringify pode ser 10 vezes mais lento que uma função, você vai memorizar.
Obviamente - a solução simples para o problema NÃO é usar uma chave de cache e NÃO acessar algum cache interno usando essa chave. Então - lembre-se dos últimos argumentos com os quais você foi chamado. Como memoizerific ou re - select do.
Memoizerific é provavelmente a única biblioteca de cache geral que você gostaria de usar.
O tamanho do cache
A segunda grande diferença entre todas as bibliotecas é sobre o tamanho do cache e a estrutura do cache.
Você já pensou - por que reselect
ou memoize-one
detém apenas um, último resultado? Não para "não usar a chave de cache para poder armazenar mais de um resultado" , mas porque não há motivos para armazenar mais do que apenas um último resultado .
... É mais sobre:
- recursos disponíveis - uma única linha de cache é muito amiga dos recursos
- padrões de uso - lembrar de algo "no lugar" é um bom padrão. Normalmente, você precisa apenas de um último resultado.
- a razão para usar -modularidade, isolamento e segurança da memória são boas razões. Não compartilhar cache com o restante do seu aplicativo é apenas mais seguro em termos de colisões de cache.
Um único resultado ?!
Sim - o único resultado. Com um resultado memorizado, algumas coisas clássicas , como a geração de números de fibonacci memorizados ( você pode encontrar como exemplo em todo artigo sobre memorização ), não seria possível . Mas, geralmente, você está fazendo outra coisa - quem precisa de um fibonacci no Frontend? No back-end? Um exemplo do mundo real está longe de ser questionário abstrato de TI .
Mas, ainda assim, existem dois GRANDES problemas sobre um tipo de memorização de valor único.
Problema 1 - é "frágil"
Por padrão - todos os argumentos devem corresponder, exatamente o mesmo "===". Se um argumento não corresponder - o jogo acabou. Mesmo que isso provenha da idéia de memorização - isso pode não ser algo que você deseja atualmente. Quero dizer - você quer memorizar o máximo possível e com a maior frequência possível.
Mesmo falta de cache é um tiro na cabeça que limpa o cache.
Há uma pequena diferença entre "hoje em dia" e "ontem" - estruturas de dados imutáveis, usadas, por exemplo, no Redux.
const getSomeDataFromState = memoize(state => compute(state.tasks));
Parece bom? Parecendo certo? No entanto, o estado pode mudar quando as tarefas não mudam, e você precisa apenas de tarefas para corresponder.
Os seletores estruturais estão aqui para salvar o dia com o seu guerreiro mais forte - a seleção de novo - à sua disposição. A nova seleção não é apenas uma biblioteca de memoização, mas seu poder vem de cascatas de memoização ou lentes (que não são, mas pensam nos seletores como lentes ópticas).
Como resultado, no caso de dados imutáveis - você sempre precisa "focar" primeiro no pedaço de dados que realmente precisa e, em seguida - executar cálculos; caso contrário, o cache será rejeitado e toda a idéia por trás da memorização desaparecerá.
Este é realmente um grande problema, especialmente para os recém-chegados, mas, como A Idéia por trás de estruturas de dados imutáveis, tem um benefício significativo - se algo não for alterado - ele não será alterado. Se algo for alterado - provavelmente será alterado . Isso nos dá uma comparação super rápida, mas com alguns falsos negativos, como no primeiro exemplo.
A idéia é "focar" nos dados dos quais você depende
Há dois momentos que eu deveria ter mencionado:
lodash.memoize
e fast-memoize
estão convertendo seus dados em uma string para ser usada como chave. Isso significa que eles são 1) não rápidos 2) não seguros 3) podem produzir falsos positivos - alguns dados diferentes podem ter a mesma representação de string . Isso pode melhorar a "taxa de cache quente", mas na verdade é uma coisa MUITO MAU.- existe uma abordagem de proxy ES6, sobre como rastrear todas as partes usadas de variáveis fornecidas e verificar apenas as chaves importantes. Embora eu pessoalmente queira criar uma infinidade de seletores de dados - talvez você não goste ou entenda o processo, mas queira ter uma memoização adequada pronta para uso -, use o estado de memorização .
Problema 2 - é "uma linha de cache"
O tamanho infinito do cache é um assassino. Qualquer cache não controlado é um assassino, desde que a memória seja bastante finita. Então - todas as melhores bibliotecas são "um cache de linha de comprimento". Essa é uma característica e uma forte decisão de design. Acabei de escrever como está correto e, acredite, é uma coisa realmente certa , mas ainda é um problema. Um grande problema.
const tasks = getTasks(state);
Uma vez que o mesmo seletor precise trabalhar com dados de origem diferentes, com mais de um - tudo está quebrado. E é fácil encontrar o problema:
- Desde que usássemos seletores para obter tarefas de um estado - poderíamos usar os mesmos seletores para obter algo de uma tarefa. Intenso vem da própria API. Mas como não funciona, você pode memorizar apenas a última chamada, mas precisa trabalhar com várias fontes de dados.
- O mesmo problema ocorre com vários componentes do React - todos iguais, e um pouco diferentes, buscando tarefas diferentes, limpando os resultados uns dos outros.
Existem 3 soluções possíveis:

Essa biblioteca ajudaria você a "manter" o cache de memorização, mas não a excluí-lo. Especialmente porque está implementando 5 (CINCO!) Estratégias de cache diferentes para atender a qualquer caso. Isso é um cheiro ruim. E se você escolher a pessoa errada?
Todos os dados que você memorizou - você precisa esquecê-los, mais cedo ou mais tarde. O ponto é não lembrar a última chamada de função - o ponto é ESQUECER no momento certo. Não muito cedo, e estragar a memorização, e não muito tarde.
Entendeu a ideia? Agora esqueça! E onde está a terceira variante?
Vamos fazer uma pausa
Parar Relaxe Respire fundo. E responda a uma pergunta simples: qual é o objetivo? O que temos que fazer para alcançar a meta? O que salvaria o dia?
DICA: Onde está esse f *** "cache" LOCALIZADO!

Onde está esse "cache" LOCALIZADO? Sim - essa é a pergunta certa. Obrigado por perguntar. E a resposta é simples - está localizada em um fechamento. Em um local escondido dentro * de uma função memorizada. Por exemplo - aqui está o código memoize-one
:
function(fn) { let lastArgs;
Você receberá um memoizedCall
e ele manterá o último resultado próximo, dentro de seu fechamento local, não acessível a ninguém, exceto memoizedCall. Um lugar seguro. "this" é um lugar seguro.
Reselect
faz o mesmo e a única maneira de criar um "fork", com outro cache - crie um novo fechamento de memorização.
Mas a (outra) questão principal - quando (cache) seria "desaparecido"?
TLDR: "ficaria" com uma função, quando a instância da função fosse consumida pelo Garbage Collector.
Instância? Instância! Então - o que acontece com a memorização por instância? Há um artigo completo sobre isso na documentação do React
Em resumo - se você estiver usando Componentes de reação com base em classe, poderá:
import memoize from "memoize-one"; class Example extends Component { filter = memoize(
Então - onde "lastResult" é armazenado? Dentro de um escopo local de filtro memorizado, dentro desta instância de classe. E quando seria "ido"?
Desta vez, "desapareceria" com uma instância de classe. Uma vez que o componente foi desmontado - ele ficou sem deixar rastro. É um "por instância" real, e você pode usar this.lastResult
para manter um resultado temporal, com exatamente o mesmo efeito de "memorização".
O que é o React.Hooks
Estamos nos aproximando. Os ganchos Redux têm alguns comandos suspeitos, que provavelmente são sobre memorização. Like - useMemo
, useCallback
, useRef

Mas a pergunta - ONDE está armazenando um valor memorizado desta vez?
Em resumo - ele o armazena em "ganchos", dentro de uma parte especial de um elemento VDOM conhecido como fibra associada a um elemento atual. Dentro de uma estrutura de dados paralela.
Os ganchos não tão curtos estão mudando a maneira como o programa funciona, movendo sua função para outra, com algumas variáveis em um local oculto dentro do fechamento dos pais . Tais funções são conhecidas como funções suspensas ou retomadas - corotinas. Em JavaScript, eles são geralmente conhecidos como generators
ou async functions
.
Mas isso é um pouco extremo. Em um uso muito curto, o Memo está armazenando valor memorizado nisso. É apenas um pouco diferente "isso".
Se queremos criar uma melhor biblioteca de memorização, devemos encontrar um "isto" melhor.
Zing!
WeakMaps!
Sim WeakMaps! Para armazenar o valor-chave, onde a chave seria essa, desde que o WeakMap não aceite nada além disso, ou seja, "objetos".
Vamos criar um exemplo simples:
const createHiddenSpot = (fn) => { const map = new WeakMap();
É estupidamente simples e bastante "certo". Então "quando isso se foi"?
- esqueça fraco Selecione e todo um "mapa" desapareceria
- esqueça todos [0] e sua fraca entrada desapareceria
- esqueça todos - e dados memorizados desapareceriam!
Está claro quando algo "foi" - somente quando deveria!
Magicamente - todos os problemas de nova seleção desapareceram. Problemas com memoização agressiva - também um caso perdido.
Essa abordagem LEMBRE - SE dos dados até a hora de ESQUECER . É inacreditável, mas para se lembrar melhor de algo, é necessário esquecê-lo melhor.
A única coisa que dura: crie uma API mais robusta para este caso
Kashe - é um cache
kashe é uma biblioteca de memorização baseada no WeakMap, que pode salvar seu dia.
Esta biblioteca expõe 4 funções
kashe
-para memorização.box
- para memorização prefixada, para aumentar a chance de memorização.inbox
- memorização prefixada aninhada, para diminuir a alteração da memorizaçãofork
- to fork (obviamente).
kashe (fn) => memoizedFn (... args)
Na verdade, é um createHiddenSpot de um exemplo anterior. Ele usará um primeiro argumento como chave para um WeakMap interno.
const selector = (state, prop) => ({result: state[prop]}); const memoized = kashe(selector); const old = memoized(state, 'x') memoized(state, 'x') === old memoized(state, 'y') === memoized(state, 'y')
O primeiro argumento é uma chave, se você chamar a função novamente da mesma chave, mas diferentes argumentos - o cache seria substituído, ainda será uma memoização de uma linha de cache. Para fazê-lo funcionar - é necessário fornecer chaves diferentes para casos diferentes, como fiz com um exemplo fraco de Select, para fornecer isso diferente para manter resultados. Selecionar novamente as cascatas A ainda é o ideal.
Nem todas as funções são kashe-memorizáveis. O primeiro argumento deve ser um objeto, matriz ou função. Deve ser utilizável como uma chave para o WeakMap.
caixa (fn) => memoizedFn2 (caixa, ... args)
esta é a mesma função, apenas aplicada duas vezes. Uma vez para fn, uma vez para memoizedFn, adicionando uma chave inicial aos argumentos. Pode tornar qualquer função kashe-memoizable.
É bastante declarativo - ei função! Vou armazenar os resultados nesta caixa.
Se você marcar a função já memorizada - você aumentará a chance de memorização, como por exemplo, memorização - você poderá criar uma cascata de memorização.
const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId);
caixa de entrada (fn) => memoizedFn2 (caixa, ... args)
este é o oposto da caixa, mas fazendo quase o mesmo, comandando o cache aninhado para armazenar dados na caixa fornecida. De um ponto de vista - reduz a probabilidade de memoização (não há cascata de memoização), mas de outro - remove as colisões de cache e ajuda a isolar os processos se eles não devem interferir um com o outro por qualquer motivo.
É bastante declarativo - ei! Todo mundo lá dentro! Aqui está uma caixa para usar
const getAndSet = (task, number) => task.value + number; const memoized = kashe(getAndSet); const inboxed = inbox(getAndSet); const doubleBoxed = inbox(memoized); memoized(task, 1)
fork (kashe-memoized) => kashe-memoized
Fork é um fork real - ele obtém qualquer função kashe-memoized e retorna a mesma, mas com outra entrada de cache interna. Lembre-se do método de fábrica redux mapStateToProps?
const mapStateToProps = () => {
Selecionar novamente
E há mais uma coisa que você deve saber - o kashe poderia substituir a nova seleção. Literalmente.
import { createSelector } from 'kashe/reselect';
Na verdade, é a mesma nova seleção, criada com o kashe como uma função de memorização.
Codesandbox
Aqui está um pequeno exemplo para brincar. Além disso, você pode checar os testes - eles são compactos e sólidos.
Se você quiser saber mais sobre armazenamento em cache e memorização - verifique como eu escrevi a biblioteca de memorização mais rápida de um ano atrás.
PS: Vale ressaltar, que a versão mais simples dessa abordagem - memoize fraca - é usada nos js de emoção por um tempo. Não há queixas. O nano-memoize também usa o WeakMaps para um único argumento.
Entendeu? Uma abordagem mais "fraca" ajudaria você a se lembrar melhor de algo e a esquecê-lo melhor.
https://github.com/theKashey/kashe
Sim, sobre o esquecimento de alguma coisa, - você poderia procurar aqui?
