
Avez-vous entendu parler de la memoization
? C'est une chose super simple, au fait, - mémorisez simplement le résultat que vous avez obtenu d'un premier appel de fonction, et utilisez-le au lieu de l'appeler la deuxième fois - n'appelez pas de vraies choses sans raison, ne perdez pas votre temps .
Ignorer certaines opérations intensives est une technique d'optimisation très courante. Chaque fois que vous ne faites pas quelque chose, ne le faites pas. Essayez d'utiliser le cache - memcache
, file cache
local cache
- n'importe quel cache! Un indispensable pour les systèmes backend et une partie cruciale de tout système backend du passé et du présent.

Mémorisation vs mise en cache
La mémorisation est comme la mise en cache. Un peu différent. Pas de cache, appelons ça kashe.
Bref, mais la mémorisation n'est pas un cache, pas un cache persistant. Il peut s'agir d'un côté serveur, mais il ne peut pas et ne doit pas être un cache côté client. Il s'agit davantage des ressources disponibles, des modèles d'utilisation et des raisons d'utiliser.
Problème - Le cache a besoin d'une "clé de cache"
Le cache stocke et récupère des données à l'aide d'une key
cache de chaîne . Construire une clé unique et utilisable est déjà un problème, mais vous devez ensuite sérialiser et désérialiser les données pour les stocker dans un support basé sur des chaînes ... en bref - le cache peut ne pas être aussi rapide que vous le pensez. Cache spécialement distribué.
La mémorisation n'a pas besoin de clé de cache
Dans le même temps - aucune clé n'est nécessaire pour la mémorisation. Habituellement, * il utilise les arguments tels quels, n'essaie pas de créer une seule clé à partir d'eux, et n'utilise pas d'objet partagé disponible globalement pour stocker les résultats, comme le fait habituellement le cache.
La différence entre la mémorisation et le cache réside dans l' API INTERFACE !
Habituellement, * ne signifie pas toujours. Lodash.memoize , par défaut, utilise JSON.stringify
pour convertir les arguments passés en cache de chaînes (existe-t-il une autre façon? Non!). Tout simplement parce qu'ils vont utiliser cette clé pour accéder à un objet interne, contenant une valeur mise en cache. fast-memoize , "la bibliothèque de mémorisation la plus rapide possible", fait de même. Les deux bibliothèques nommées ne sont pas des bibliothèques de mémorisation, mais des bibliothèques de cache.
Il vaut la peine de mentionner - JSON.stringify pourrait être 10 fois plus lent qu'une fonction, tu vas mémoriser.
De toute évidence - la solution simple au problème n'est PAS d'utiliser une clé de cache, et PAS d'accéder à un cache interne en utilisant cette clé. Alors - souvenez-vous des derniers arguments avec lesquels vous avez été appelé. Comme faire mémoriser ou resélectionner .
Memoizerific est probablement la seule bibliothèque de mise en cache générale que vous souhaitez utiliser.
La taille du cache
La deuxième grande différence entre toutes les bibliothèques concerne la taille du cache et la structure du cache.
Avez-vous déjà pensé - pourquoi reselect
ou memoize-one
ne contient qu'un, dernier résultat? Pas pour «ne pas utiliser la clé de cache pour pouvoir stocker plus d'un résultat» , mais parce qu'il n'y a aucune raison de stocker plus qu'un dernier résultat .
... Il s'agit plus de:
- ressources disponibles - une seule ligne de cache est très conviviale pour les ressources
- modèles d'utilisation - se souvenir de quelque chose «en place» est un bon modèle. "En place", vous n'avez généralement besoin que d'un dernier résultat.
- la raison d'utiliser -modularité, isolation et sécurité de la mémoire sont de bonnes raisons. Ne pas partager le cache avec le reste de votre application est tout simplement plus sûr en termes de collisions de cache.
Un seul résultat?!
Oui - le seul résultat. Avec un résultat mémorisé certaines choses classiques , comme la génération de nombre de fibonacci mémorisé ( vous pouvez trouver comme exemple dans chaque article sur la mémorisation ) ne serait pas possible . Mais, d'habitude, vous faites autre chose - qui a besoin d'un fibonacci sur Frontend? En backend? Les exemples du monde réel sont assez loin des quiz informatiques abstraits.
Mais encore, il y a deux gros problèmes à propos d'un type de mémorisation à valeur unique.
Problème 1 - c'est "fragile"
Par défaut - tous les arguments doivent correspondre, être exactement les mêmes "===". Si un argument ne correspond pas - le jeu est terminé. Même si cela vient de l'idée de la mémorisation - ce n'est peut-être pas quelque chose que vous voulez de nos jours. Je veux dire - vous voulez mémoriser autant de choses que possible et aussi souvent que possible.
Même le cache manquant est un tir à la tête de nettoyage du cache.
Il y a une petite différence entre "de nos jours" et "hier" - des structures de données immuables, utilisées par exemple dans Redux.
const getSomeDataFromState = memoize(state => compute(state.tasks));
Vous cherchez bien? Vous cherchez bien? Cependant, l'état peut changer lorsque les tâches ne le sont pas et vous n'avez besoin que de tâches pour correspondre.
Les sélecteurs structurels sont là pour sauver la situation avec leur guerrier le plus fort - Reselect - à votre disposition et appelez. Reselect n'est pas seulement une bibliothèque de mémorisation, mais sa puissance provient de cascades de mémorisation ou d'objectifs (ce qu'ils ne sont pas, mais considérez les sélecteurs comme des lentilles optiques).
En conséquence, dans le cas de données immuables - vous devez toujours vous "concentrer" d'abord sur la donnée dont vous avez vraiment besoin, puis - effectuer des calculs, sinon le cache serait rejeté, et toute l'idée derrière la mémorisation disparaîtrait.
C'est en fait un gros problème, en particulier pour les nouveaux arrivants, mais cela, comme L'idée derrière des structures de données immuables, présente un avantage significatif - si quelque chose n'est pas changé - il n'est pas changé. Si quelque chose est changé - probablement il est changé . Cela nous donne une comparaison super rapide, mais avec quelques faux négatifs, comme dans le premier exemple.
L'idée consiste à "se concentrer" sur les données dont vous dépendez
Il y a deux moments que j'aurais dû - mentionnés:
lodash.memoize
et fast-memoize
convertissent vos données en une chaîne à utiliser comme clé. Cela signifie qu'ils sont 1) pas rapides 2) pas sûrs 3) pourraient produire des faux positifs - certaines données différentes pourraient avoir la même représentation sous forme de chaîne . Cela pourrait améliorer le "taux chaud du cache", mais c'est en fait TRÈS MAUVAIS.- il existe une approche ES6 Proxy, sur le suivi de toutes les parties de variables utilisées données, et la vérification des clés qui comptent. Bien que je souhaite personnellement créer des myriades de sélecteurs de données - vous pourriez ne pas aimer ou comprendre le processus, mais souhaiteriez avoir une mémorisation appropriée dès le départ - puis utilisez memoize-state .
Problème 2- c'est "une ligne de cache"
La taille infinie du cache est un tueur. Tout cache non contrôlé est un tueur, tant que la mémoire est assez limitée. Donc - toutes les meilleures bibliothèques sont «à une ligne de cache». C'est une caractéristique et une décision de conception solide. Je viens d'écrire à quel point c'est juste et, croyez-moi - c'est vraiment une bonne chose , mais c'est toujours un problème. Un gros problème.
const tasks = getTasks(state);
Une fois que le même sélecteur doit travailler avec différentes données source, avec plus d'un - tout est cassé. Et il est facile de rencontrer le problème:
- Tant que nous utilisions des sélecteurs pour obtenir des tâches d'un état - nous pouvions utiliser les mêmes sélecteurs pour obtenir quelque chose d'une tâche. Intense vient de l'API elle-même. Mais cela ne fonctionne pas, vous ne pouvez mémoriser que le dernier appel, mais vous devez travailler avec plusieurs sources de données.
- Le même problème se pose avec plusieurs composants React - ils sont tous les mêmes, et tous un peu différents, récupérant des tâches différentes, effaçant les résultats les uns des autres.
Il existe 3 solutions possibles:

Cette bibliothèque vous aiderait à "conserver" le cache de mémorisation, mais pas à le supprimer. Surtout parce qu'il implémente 5 (CINQ!) Différentes stratégies de cache pour s'adapter à tous les cas. C'est une mauvaise odeur. Et si vous choisissez le mauvais?
Toutes les données que vous avez mémorisées - vous devez les oublier, tôt ou tard. Il ne s'agit pas de se souvenir de la dernière invocation de fonction - il s'agit de L'OUBLIER au bon moment. Pas trop tôt, et ruiner la mémorisation, et pas trop tard.
Vous avez l'idée? Maintenant, oublie ça! Et où est la 3ème variante ??
Laisse faire une pause
Arrêter Détendez-vous Respirez profondément. Et répondez à une question simple - Quel est l'objectif? Que devons-nous faire pour atteindre l'objectif? Qu'est-ce qui sauverait la journée?
CONSEIL: Où est cette f *** "cache" SITUÉE!

Où est situé ce "cache"? Oui - c'est la bonne question. Merci de le demander. Et la réponse est simple - elle est située dans une fermeture. Dans un endroit caché à l'intérieur * une fonction mémorisée. Par exemple - voici le code memoize-one
:
function(fn) { let lastArgs;
Vous recevrez un memoizedCall
, et il tiendra le dernier résultat à proximité, à l'intérieur de sa fermeture locale, non accessible à personne, sauf memoizedCall. Un endroit sûr. "ceci" est un endroit sûr.
Reselect
fait de même, et la seule façon de créer un "fork", avec un autre cache - créer une nouvelle fermeture de mémorisation.
Mais la (autre) question principale - quand (le cache) serait-il "parti"?
TLDR: Il serait "disparu" avec une fonction, lorsque l'instance de fonction serait mangée par Garbage Collector.
Instance? Instance! Alors, qu'en est-il de la mémorisation par instance? Il y a tout un article à ce sujet dans la documentation React
En bref - si vous utilisez des composants React basés sur les classes, vous pouvez faire:
import memoize from "memoize-one"; class Example extends Component { filter = memoize(
Alors - où "lastResult" est stocké? À l'intérieur d'une portée locale de filtre mémorisé, à l'intérieur de cette instance de classe. Et, quand ce serait "parti"?
Cette fois, il "serait parti" avec une instance de classe. Une fois le composant démonté, il est parti sans laisser de trace. C'est un véritable "par instance", et vous pouvez utiliser this.lastResult
pour conserver un résultat temporel, avec exactement le même effet de "mémorisation".
À propos de React.Hooks
Nous nous rapprochons. Les hooks Redux ont quelques commandes suspectes, qui concernent probablement la mémorisation. Comme - useMemo
, useCallback
, useRef

Mais la question - O it il stocke une valeur mémorisée cette fois?
En bref - il le stocke dans des "crochets", à l'intérieur d'une partie spéciale d'un élément VDOM appelé fibre associée à un élément courant. Au sein d'une structure de données parallèle.
Pas si court - les hooks changent la façon dont votre programme fonctionne, déplaçant votre fonction à l'intérieur d'une autre, avec quelques variables dans un endroit caché à l'intérieur d'une fermeture parent . Ces fonctions sont connues sous le nom de fonctions suspendables ou pouvant être reprises - coroutines. En JavaScript, ils sont généralement appelés generators
ou async functions
.
Mais c'est un peu extrême. Dans un très court - useMemo stocke la valeur mémorisée dans cela. C'est juste un peu différent "ceci".
Si nous voulons créer une meilleure bibliothèque de mémorisation, nous devons trouver un meilleur "ceci".
Zing!
WeakMaps!
Oui! WeakMaps! Pour stocker la valeur-clé, où la clé serait celle-ci, tant que WeakMap n'accepte rien d'autre que cela, c'est-à-dire des "objets".
Créons un exemple simple:
const createHiddenSpot = (fn) => { const map = new WeakMap();
C'est stupidement simple et tout à fait "juste". Alors "quand ça serait parti"?
- oublier faibleSélectionner et toute une "carte" serait partie
- oublier todos [0] et leur entrée faible serait partie
- oubliez les todos - et les données mémorisées auraient disparu!
Il est clair que quelque chose «disparaît» - seulement quand cela devrait!
Magiquement - tous les problèmes de resélection ont disparu. Problèmes de mémorisation agressive - également un enfer.
Cette approche N'OUBLIEZ PAS les données jusqu'à ce qu'il soit temps d' OUBLIER . C'est incroyable, mais pour mieux se souvenir de quelque chose, il faut pouvoir mieux l'oublier.
La seule chose qui dure - créer une API plus robuste pour ce cas
Kashe - est une cache
kashe est une bibliothèque de mémorisation basée sur WeakMap, qui pourrait vous sauver la vie.
Cette bibliothèque expose 4 fonctions
kashe
-pour la kashe
.box
- pour la mémorisation préfixée, pour augmenter les chances de mémorisation.inbox
- mémorisation préfixée imbriquée, pour diminuer le changement de mémorisationfork
- to fork (évidemment) la mémorisation.
kashe (fn) => memoizedFn (... args)
Il s'agit en fait d'un createHiddenSpot d'un exemple précédent. Il utilisera un premier argument comme clé pour un WeakMap interne.
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')
le premier argument est une clé, si vous appelez à nouveau fonction la même clé, mais des arguments différents - le cache serait remplacé, c'est toujours une mémorisation longue d'une ligne de cache. Pour le faire fonctionner - vous devez fournir différentes clés pour différents cas, comme je l'ai fait avec un exemple de faiblesse, pour fournir des résultats différents. Resélectionnez les cascades A est toujours la chose.
Toutes les fonctions ne sont pas mémorisables. Le premier argument doit être un objet, un tableau ou une fonction. Il devrait être utilisable comme clé pour WeakMap.
box (fn) => memoizedFn2 (box, ... args)
c'est la même fonction, juste appliquée deux fois. Une fois pour fn, une fois pour memoizedFn, en ajoutant une clé de tête aux arguments. Cela pourrait rendre n'importe quelle fonction mémorable.
C'est assez déclaratif - hé fonction! Je vais stocker les résultats dans cette case.
Si vous encadrez la fonction déjà mémorisée - vous augmentez les chances de mémorisation, comme la mémorisation par instance - vous pouvez créer une cascade de mémorisation.
const selectSomethingFromTodo = (state, prop) => ... const selector = kashe(selectSomethingFromTodo); const boxedSelector = kashe(selector); class Component { render () { const result = boxedSelector(this, todos, this.props.todoId);
boîte de réception (fn) => memoizedFn2 (box, ... args)
celui-ci est opposé à la boîte, mais fait presque la même chose, commandant au cache imbriqué de stocker les données dans la boîte fournie. D'un point de vue - cela réduit la probabilité de mémorisation (il n'y a pas de cascade de mémorisation), mais d'un autre - il supprime les collisions de cache et aide à isoler les processus s'ils ne doivent pas interférer les uns avec les autres pour quelque raison que ce soit.
C'est assez déclaratif - hé! Tout le monde à l'intérieur! Voici une boite à utiliser
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 est un vrai fork - il obtient n'importe quelle fonction mémorisée par kashe et retourne la même chose, mais avec une autre entrée de cache interne. Rappelez-vous la méthode d'usine redux mapStateToProps?
const mapStateToProps = () => {
Resélectionnez
Et il y a encore une chose que vous devez savoir - kashe pourrait remplacer resélectionner. Littéralement.
import { createSelector } from 'kashe/reselect';
C'est en fait la même resélection, juste créée avec kashe comme fonction de mémorisation.
Codesandbox
Voici un petit exemple pour jouer avec. Vous pouvez également vérifier les tests - ils sont compacts et solides.
Si vous voulez en savoir plus sur la mise en cache et la mémorisation - vérifiez comment j'ai écrit la bibliothèque de mémorisation la plus rapide il y a un an.
PS: Il convient de mentionner que la version la plus simple de cette approche - faible-mémorisation - est utilisée dans emotion-js pendant un certain temps. Rien à redire. nano-memoize utilise également WeakMaps pour un cas d'argument unique.
Vous avez compris? Une approche plus «faible» vous aiderait à mieux vous souvenir de quelque chose et à mieux l'oublier.
https://github.com/theKashey/kashe
Ouais, au sujet d'oublier quelque chose, - pourriez-vous s'il vous plaît regarder ici?
