
¿Has oído hablar de la memoization
? Por cierto, es una cosa súper simple: solo memorice el resultado que obtuvo de una primera llamada de función y úselo en lugar de llamarlo la segunda vez, no llame cosas reales sin razón, no pierda su tiempo .
Saltarse algunas operaciones intensivas es una técnica de optimización muy común. Cada vez que no hagas algo, no lo hagas. Intente usar caché: memcache
, file cache
, local cache
, ¡cualquier caché! Una herramienta imprescindible para los sistemas de back-end y una parte crucial de cualquier sistema de back-end del pasado y el presente.

Memoization vs Caching
La memorización es como el almacenamiento en caché. Solo un poco diferente. No caché, llamémoslo Kashe.
Larga historia corta, pero la memorización no es un caché, no un caché persistente. Puede ser en el lado del servidor, pero no puede y no debe ser un caché en el lado del cliente. Se trata más de los recursos disponibles, los patrones de uso y las razones para usarlos.
Problema: la caché necesita una "clave de caché"
La memoria caché almacena y recupera datos utilizando una key
cadena de caché. Ya es un problema construir una clave única y utilizable, pero luego hay que serializar y eliminar la serialización de datos para almacenarlos nuevamente en un medio basado en cadenas ... en resumen, la caché puede no ser tan rápida, como podría pensar. Caché especialmente distribuido.
La memorización no necesita ninguna clave de caché
Al mismo tiempo, no se necesita ninguna clave para la memorización. Por lo general, * usa argumentos tal cual, no intenta crear una sola clave a partir de ellos, y no usa algún objeto compartido disponible globalmente para almacenar resultados, como suele hacer la memoria caché.
¡La diferencia entre la memorización y el caché está en la interfaz API !
Por lo general, * no significa siempre. Lodash.memoize , por defecto, usa JSON.stringify
para convertir los argumentos pasados en un caché de cadenas (¿hay alguna otra forma? ¡No!). Solo porque van a usar esta clave para acceder a un objeto interno, que tiene un valor en caché. fast-memoize , "la biblioteca de memorización más rápida posible", hace lo mismo. Ambas bibliotecas con nombre no son bibliotecas de memoria, sino bibliotecas de caché.
Vale la pena mencionar que JSON.stringify podría ser 10 veces más lento que una función, vas a memorizar.
Obviamente, la solución simple al problema NO es usar una clave de caché, y NO acceder a alguna caché interna usando esa clave. Entonces, recuerde los últimos argumentos con los que fue llamado. Como memoizerific o reselect do.
Memoizerific es probablemente la única biblioteca de almacenamiento en caché general que le gustaría usar.
El tamaño de caché
La segunda gran diferencia entre todas las bibliotecas es sobre el tamaño del caché y la estructura del caché.
¿Alguna vez has pensado: por qué reselect
o memoize-one
solo tiene un último resultado? No para "no usar la clave de caché para poder almacenar más de un resultado" , sino porque no hay razones para almacenar más que un último resultado .
... Se trata más de:
- recursos disponibles: una sola línea de caché es muy amigable con los recursos
- patrones de uso: recordar algo "en su lugar" es un buen patrón. "En su lugar" generalmente solo necesita un último resultado.
- La razón para usar -modularidad, aislamiento y seguridad de la memoria son buenas razones. No compartir caché con el resto de su aplicación es más seguro en términos de colisiones de caché.
¿Un solo resultado?
Sí, el único resultado. Con un resultado memorizado, algunas cosas clásicas , como la generación memorizada de números de Fibonacci ( puede encontrar como ejemplo en cada artículo sobre la memorización ) no sería posible . Pero, por lo general, estás haciendo otra cosa: ¿quién necesita un fibonacci en Frontend? En el backend? Un ejemplo del mundo real está bastante lejos de los cuestionarios abstractos de TI .
Pero aún así, hay dos GRANDES problemas sobre un tipo de memoria de valor único.
Problema 1: es "frágil"
Por defecto, todos los argumentos deben coincidir, exactamente "===" igual. Si un argumento no coincide, el juego ha terminado. Incluso si esto proviene de la idea de la memorización, puede que no sea algo que desee hoy en día. Quiero decir, quieres memorizar tanto como sea posible y con la mayor frecuencia posible.
Incluso la falta de caché es un disparo en la cabeza que borra el caché.
Hay una pequeña diferencia entre "hoy en día" y "ayer": estructuras de datos inmutables, que se utilizan, por ejemplo, en Redux.
const getSomeDataFromState = memoize(state => compute(state.tasks));
¿Te ves bien? Mirando bien? Sin embargo, el estado puede cambiar cuando las tareas no lo hicieron, y solo necesita que las tareas coincidan.
Los Selectores estructurales están aquí para salvar el día con su guerrero más fuerte, Reselect , a su entera disposición. Reselect no es solo una biblioteca de memorización, sino que su potencia proviene de cascadas de memorización o lentes (que no lo son, pero piense en los selectores como lentes ópticos).
Como resultado, en el caso de los datos inmutables, siempre debe primero "enfocarse" en la pieza de datos que realmente necesita, y luego, realizar cálculos, o de lo contrario la memoria caché sería rechazada, y toda la idea detrás de la memorización desaparecería.
Esto es realmente un gran problema, especialmente para los recién llegados, pero, como La idea detrás de las estructuras de datos inmutables, tiene un beneficio significativo, si algo no cambia, no cambia. Si se cambia algo, probablemente se cambie . Eso nos da una comparación súper rápida, pero con algunos falsos negativos, como en el primer ejemplo.
La idea es "enfocarse" en los datos de los que depende
Hay dos momentos que debería haber mencionado:
lodash.memoize
y fast-memoize
están convirtiendo sus datos en una cadena para usar como clave. Eso significa que son 1) no rápidos 2) no seguros 3) podrían producir falsos positivos: algunos datos diferentes podrían tener la misma representación de cadena . Esto podría mejorar la "velocidad de caché", pero en realidad es algo MUY MALO.- existe un enfoque de Proxy ES6, sobre el seguimiento de todas las variables utilizadas dadas y la verificación de solo las claves que importan. Si bien personalmente me gustaría crear una miríada de selectores de datos, es posible que no le guste o entienda el proceso, pero que desee tener una memorización adecuada de fábrica, luego use memoize-state .
Problema 2- es "una línea de caché"
El tamaño de caché infinito es un asesino. Cualquier caché no controlado es un asesino, siempre que la memoria sea bastante finita. Entonces, todas las mejores bibliotecas son "one-cache-line-long". Esa es una característica y una fuerte decisión de diseño. Acabo de escribir lo correcto que es y, créeme, es algo realmente correcto , pero sigue siendo un problema. Un gran problema
const tasks = getTasks(state);
Una vez que el mismo selector tiene que trabajar con diferentes datos de origen, con más de uno, todo se rompe. Y es fácil encontrarse con el problema:
- Mientras estuviéramos usando selectores para obtener tareas de un estado, podríamos usar los mismos selectores para obtener algo de una tarea. Intense proviene de la propia API. Pero no funciona, entonces puede memorizar solo la última llamada, pero debe trabajar con múltiples fuentes de datos.
- El mismo problema es con múltiples componentes React: todos son iguales y todos un poco diferentes, obteniendo diferentes tareas, borrando los resultados entre sí.
Hay 3 posibles soluciones:

Esta biblioteca lo ayudaría a "mantener" la memoria caché de la memoria, pero no a eliminarla. Especialmente porque está implementando 5 (¡CINCO!) Diferentes estrategias de caché para adaptarse a cualquier caso. Eso es un mal olor. ¿Qué pasa si eliges el equivocado?
Todos los datos que ha memorizado: debe olvidarlos, tarde o temprano. El punto no es recordar la invocación de la última función; el punto es OLVIDARLO en el momento adecuado. No demasiado pronto, y arruinar la memorización, y no demasiado tarde.
¿Tienes la idea? Ahora olvídalo! ¿Y dónde está la tercera variante?
Deja una pausa
Para Relajarse Respira hondo. Y responde una pregunta simple: ¿Cuál es el objetivo? ¿Qué tenemos que hacer para alcanzar la meta? ¿Qué salvaría el día?
CONSEJO: ¿Dónde está ese maldito "caché" UBICADO!

¿Dónde está ubicado ese "caché"? Sí, esa es la pregunta correcta. Gracias por preguntar Y la respuesta es simple: se encuentra en un cierre. En un lugar oculto dentro * una función memorable. Por ejemplo, aquí está el código memoize-one
:
function(fn) { let lastArgs;
Recibirá una memoizedCall
, y mantendrá el último resultado cerca, dentro de su cierre local, al que nadie podrá acceder, excepto memoizedCall. Un lugar seguro "este" es un lugar seguro.
Reselect
hace lo mismo, y la única forma de crear una "bifurcación", con otro caché: crear un nuevo cierre de memorización.
Pero la (otra) pregunta principal: ¿cuándo (caché) se habría "ido"?
TLDR: "desaparecería" con una función, cuando Garbage Collector se comiera la instancia de la función.
Instancia? Instancia! Entonces, ¿qué pasa con la memorización por instancia? Hay un artículo completo al respecto en la documentación de React
En resumen: si está utilizando React Components basados en clases, puede hacer lo siguiente:
import memoize from "memoize-one"; class Example extends Component { filter = memoize(
Entonces, ¿dónde se almacena "lastResult" ? Dentro de un ámbito local de filtro memorizado, dentro de esta instancia de clase. ¿Y cuándo se iría?
Esta vez "se iría" con una instancia de clase. Una vez que el componente se desmontó, desapareció sin dejar rastro. Es un verdadero "por instancia", y puede usar this.lastResult
para mantener un resultado temporal, con exactamente el mismo efecto de "memorización".
¿Qué hay de reaccionar?
Nos estamos acercando. Los ganchos de Redux tienen algunos comandos sospechosos, que, probablemente, son sobre memorización. Me gusta: useMemo
, useCallback
, useRef

Pero la pregunta: ¿DÓNDE está almacenando un valor memorable esta vez?
En resumen: lo almacena en "ganchos", dentro de una parte especial de un elemento VDOM conocido como fibra asociada con un elemento actual. Dentro de una estructura de datos paralela.
No tan corto: los ganchos están cambiando la forma en que funciona su programa, moviendo su función dentro de otra, con algunas variables en un lugar oculto dentro de un cierre principal . Dichas funciones se conocen como funciones suspendibles o reanudables : corutinas. En JavaScript, generalmente se los conoce como generators
o async functions
.
Pero eso es un poco extremo. En muy poco tiempo, useMemo está almacenando un valor memorable en esto. Es un poco diferente "esto".
Si queremos crear una mejor biblioteca de memorización, deberíamos encontrar un mejor "esto".
Zing!
WeakMaps!
Si! WeakMaps! Para almacenar el valor-clave, donde la clave sería esta, siempre que WeakMap no acepte nada excepto esto, es decir, "objetos".
Creemos un ejemplo simple:
const createHiddenSpot = (fn) => { const map = new WeakMap();
Es estúpidamente simple y bastante "correcto". Entonces, ¿cuándo se iría?
- olvides débilSeleccione y todo un "mapa" desaparecería
- olviden a todos [0] y su entrada débil se habría ido
- olvídate de todos, ¡y los datos memorizados desaparecerían!
Está claro cuándo algo "desaparecería", ¡solo cuando debería!
Mágicamente, todos los problemas de reselección se han ido. Problemas con la memorización agresiva, también un goner.
Este enfoque RECUERDA los datos hasta que sea hora de OLVIDAR . Es increíble, pero para recordar mejor algo tienes que poder olvidarlo mejor.
Lo único dura: cree una API más robusta para este caso
Kashe - es un caché
Kashe es una biblioteca de memorización basada en WeakMap, que podría salvar su día.
Esta biblioteca expone 4 funciones.
kashe
-para memorización.box
: para la memorización prefijada, para aumentar las posibilidades de memorización.inbox
- memorización prefijada anidada, para disminuir el cambio de memorizaciónfork
- a tenedor (obviamente) la memorización.
kashe (fn) => memoizedFn (... args)
En realidad, es createHiddenSpot de un ejemplo anterior. Utilizará un primer argumento como clave para un 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')
el primer argumento es una clave, si llama a la función nuevamente la misma clave, pero diferentes argumentos: la memoria caché se reemplazaría, todavía es una larga memoria de línea de caché. Para que funcione, debe proporcionar diferentes claves para diferentes casos, como hice con un ejemplo débilSelect, para proporcionar diferentes para mantener los resultados. Volver a seleccionar las cascadas A sigue siendo la cosa.
No todas las funciones son kashe-memorables. El primer argumento tiene que ser un objeto, una matriz o una función. Debería ser utilizable como clave para WeakMap.
box (fn) => memoizedFn2 (box, ... args)
Esta es la misma función, solo se aplica dos veces. Una vez para fn, una vez para memoizedFn, agregando una clave principal a los argumentos. Puede hacer que cualquier función sea memorable.
Es bastante declarativo: ¡hey, función! Guardaré los resultados en este cuadro.
Si encajona la función ya memorizada, aumentará las posibilidades de memorización, como la memorización por instancia, puede crear una cascada de memorización.
const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId);
inbox (fn) => memoizedFn2 (box, ... args)
este es opuesto al cuadro, pero hace casi lo mismo, ordenando el caché anidado para almacenar datos en el cuadro provisto. Desde un punto de vista, reduce la probabilidad de memorización (no hay una cascada de memorización), pero desde otro, elimina las colisiones de caché y ayuda a aislar procesos si no interfieren entre sí por algún motivo.
Es bastante declarativo, ¡oye! ¡Todos adentro! Aquí hay una caja 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
La bifurcación es una bifurcación real: obtiene cualquier función memorizada por Kashe y devuelve la misma, pero con otra entrada de caché interna. ¿Recuerdas el método de fábrica redux mapStateToProps?
const mapStateToProps = () => {
Reseleccionar
Y hay una cosa más que debes saber: Kashe podría reemplazar la reselección. Literalmente
import { createSelector } from 'kashe/reselect';
En realidad, es la misma reselección, recién creada con kashe como función de memorización.
Códigos y caja
Aquí hay un pequeño ejemplo para jugar. También puede verificar dos veces las pruebas : son compactas y sólidas.
Si desea saber más sobre el almacenamiento en caché y la memorización, compruebe cómo escribí la biblioteca de memorización más rápida hace un año.
PD: Vale la pena mencionar, que la versión más simple de este enfoque - débil-memorizar - se utiliza en la emoción-js por un tiempo. No tengo quejas nano-memoize también usa WeakMaps para un caso de argumento único.
¿Entendido? Un enfoque más "débil" lo ayudaría a recordar mejor algo y olvidarlo mejor.
https://github.com/theKashey/kashe
Sí, sobre olvidar algo, ¿podrías mirar aquí?
