Quelles sont les utilisations réelles de la structure de données WeakMap
introduite dans ECMAScript 6?
Etant donné qu'une clé d'une carte faible crée une forte référence à sa valeur correspondante, garantissant qu'une valeur insérée dans une carte faible jamais disparaît tant que sa clé est toujours vivante, elle peut ' ne pas être utilisé pour les tableaux de notes, les caches ou toute autre chose pour laquelle vous utiliseriez normalement des références faibles, des cartes avec des valeurs faibles, etc. pour.
Il me semble que ceci:
weakmap.set(key, value);
... est juste une façon détournée de dire ceci:
key.value = value;
Quels cas d'utilisation concrets me manque?
Les WeakMaps fournissent un moyen d'étendre des objets de l'extérieur sans interférer avec le garbage collection. Chaque fois que vous voulez étendre un objet mais que vous ne pouvez pas le faire car il est scellé - ou à partir d'une source externe - un WeakMap peut être appliqué .
Un WeakMap est une carte (dictionnaire) où les clés sont faibles - c'est-à-dire si toutes les références à clé sont perdues et qu'il n'y a plus de références à la valeur - le valeur peut être nettoyé. Montrons ceci d'abord à travers des exemples, puis expliquons-le un peu et finissons avec une utilisation réelle.
Disons que j'utilise une API qui me donne un certain objet:
var obj = getObjectFromLibrary();
Maintenant, j'ai une méthode qui utilise l'objet:
function useObj(obj){
doSomethingWith(obj);
}
Je veux garder une trace de combien de fois la méthode a été appelée avec un certain objet et signaler si cela se produit plus de N fois. Naïvement on penserait utiliser une carte:
var map = new Map(); // maps can have object keys
function useObj(obj){
doSomethingWith(obj);
var called = map.get(obj) || 0;
called++; // called one more time
if(called > 10) report(); // Report called more than 10 times
map.set(obj, called);
}
Cela fonctionne, mais il y a une fuite de mémoire - nous gardons maintenant une trace de chaque objet de bibliothèque transmis à la fonction, ce qui empêche les objets de bibliothèque d'être collectés. Au lieu de cela - nous pouvons utiliser un WeakMap
:
var map = new WeakMap(); // create a weak map
function useObj(obj){
doSomethingWith(obj);
var called = map.get(obj) || 0;
called++; // called one more time
if(called > 10) report(); // Report called more than 10 times
map.set(obj, called);
}
Et la fuite de mémoire est partie.
Certains cas d'utilisation susceptibles de provoquer une fuite de mémoire et activés par WeakMap
s incluent:
Il peut être utilisé pour étendre un objet de l'extérieur. Donnons un exemple pratique (adapté, en quelque sorte réel - pour faire valoir un point) du monde réel de Node.js.
Supposons que vous soyez Node.js et que vous ayez des objets Promise
- vous souhaitez maintenant garder une trace de toutes les promesses actuellement rejetées - cependant, vous faites not vous voulez évitez de les ramasser au cas où il n’y aurait aucune référence.
Maintenant, vous don't == voulez ajouter des propriétés à des objets natifs pour des raisons évidentes - vous êtes donc bloqué. Si vous gardez des références aux promesses, vous causez une fuite de mémoire car aucun garbage collection ne peut se produire. Si vous ne conservez pas de références, vous ne pouvez pas enregistrer d'informations supplémentaires sur les promesses individuelles. Tout schéma qui implique l’enregistrement de l’ID d’une promesse signifie de manière inhérente que vous avez besoin d’une référence.
WeakMaps signifie que les touches sont faibles. Il n’existe aucun moyen d’énumérer une carte faible ou d’obtenir toutes ses valeurs. Dans une carte faible, vous pouvez stocker les données en fonction d'une clé et, lorsque les clés sont collectées, les valeurs le sont également.
Cela signifie que si vous avez une promesse, vous pouvez stocker un état à ce sujet - et cet objet peut toujours être récupéré. Plus tard, si vous obtenez une référence à un objet, vous pouvez vérifier si vous avez un état le concernant et le signaler.
Ceci a été utilisé pour implémenter crochets de rejet non gérés par Petka Antonov en tant que this :
process.on('unhandledRejection', function(reason, p) {
console.log("Unhandled Rejection at: Promise ", p, " reason: ", reason);
// application specific logging, throwing an error, or other logic here
});
Nous conservons les informations sur les promesses sur une carte et pouvons savoir quand une promesse rejetée a été traitée.
Un cas d'utilisation pourrait être de l'utiliser comme dictionnaire pour les auditeurs, j'ai un collègue qui l'a fait. C'est très utile, car tout auditeur est directement concerné par cette façon de faire. Au revoir listener.on
.
Mais d'un point de vue plus abstrait, WeakMap
est particulièrement puissant pour dématérialiser l'accès à n'importe quoi, vous n'avez pas besoin d'un espace de noms pour isoler ses membres, car il est déjà impliqué dans la nature de cette structure. Je suis presque certain que vous pourriez apporter des améliorations majeures à la mémoire en remplaçant les clés d’objet redondantes (bien que la déconstruction fasse le travail à votre place).
Je réalise maintenant que mon accent n'est pas exactement la meilleure façon de traiter le problème et, comme Benjamin Gruenbaum l'a fait remarquer (consultez sa réponse, si elle n'est pas déjà au-dessus de la mienne: p), ce problème n'aurait pas pu a été résolu avec un Map
régulier, car il aurait coulé. La principale force de WeakMap
est donc de ne pas interférer avec le nettoyage des ordures, car ils ne gardent pas de référence.
Voici le code actuel de mon collègue (merci à lui pour le partage)
Source complète ici , il s'agit de la gestion des auditeurs dont j'ai parlé ci-dessus (vous pouvez également jeter un oeil à la specs )
var listenableMap = new WeakMap();
export function getListenable (object) {
if (!listenableMap.has(object)) {
listenableMap.set(object, {});
}
return listenableMap.get(object);
}
export function getListeners (object, identifier) {
var listenable = getListenable(object);
listenable[identifier] = listenable[identifier] || [];
return listenable[identifier];
}
export function on (object, identifier, listener) {
var listeners = getListeners(object, identifier);
listeners.Push(listener);
}
export function removeListener (object, identifier, listener) {
var listeners = getListeners(object, identifier);
var index = listeners.indexOf(listener);
if(index !== -1) {
listeners.splice(index, 1);
}
}
export function emit (object, identifier, ...args) {
var listeners = getListeners(object, identifier);
for (var listener of listeners) {
listener.apply(object, args);
}
}
WeakMap
fonctionne bien pour l'encapsulation et la dissimulation d'informations
WeakMap
n'est disponible que pour ES6 et les versions ultérieures. Un WeakMap
est un ensemble de paires clé/valeur dans lesquelles la clé doit être un objet. Dans l'exemple suivant, nous construisons un WeakMap
avec deux éléments:
var map = new WeakMap();
var pavloHero = {first: "Pavlo", last: "Hero"};
var gabrielFranco = {first: "Gabriel", last: "Franco"};
map.set(pavloHero, "This is Hero");
map.set(gabrielFranco, "This is Franco");
console.log(map.get(pavloHero));//This is Hero
Nous avons utilisé la méthode set()
pour définir une association entre un objet et un autre élément (une chaîne dans notre cas). Nous avons utilisé la méthode get()
pour récupérer l'élément associé à un objet. L'aspect intéressant de la WeakMap
s est le fait qu'il contient une faible référence à la clé à l'intérieur de la carte. Une référence faible signifie que si l'objet est détruit, le ramasse-miettes supprimera toute l'entrée du fichier WeakMap
, libérant ainsi de la mémoire.
var TheatreSeats = (function() {
var priv = new WeakMap();
var _ = function(instance) {
return priv.get(instance);
};
return (function() {
function TheatreSeatsConstructor() {
var privateMembers = {
seats: []
};
priv.set(this, privateMembers);
this.maxSize = 10;
}
TheatreSeatsConstructor.prototype.placePerson = function(person) {
_(this).seats.Push(person);
};
TheatreSeatsConstructor.prototype.countOccupiedSeats = function() {
return _(this).seats.length;
};
TheatreSeatsConstructor.prototype.isSoldOut = function() {
return _(this).seats.length >= this.maxSize;
};
TheatreSeatsConstructor.prototype.countFreeSeats = function() {
return this.maxSize - _(this).seats.length;
};
return TheatreSeatsConstructor;
}());
})()
J'utilise WeakMap
pour la mémoire cache de mémoization sans souci de fonctions prenant en paramètre des objets immuables.
La mémorisation est une manière élégante de dire "après avoir calculé la valeur, mettez-la en cache pour ne pas avoir à la calculer à nouveau".
Voici un exemple:
// using immutable.js from here https://facebook.github.io/immutable-js/
const memo = new WeakMap();
let myObj = Immutable.Map({a: 5, b: 6});
function someLongComputeFunction (someImmutableObj) {
// if we saved the value, then return it
if (memo.has(someImmutableObj)) {
console.log('used memo!');
return memo.get(someImmutableObj);
}
// else compute, set, and return
const computedValue = someImmutableObj.get('a') + someImmutableObj.get('b');
memo.set(someImmutableObj, computedValue);
console.log('computed value');
return computedValue;
}
someLongComputeFunction(myObj);
someLongComputeFunction(myObj);
someLongComputeFunction(myObj);
// reassign
myObj = Immutable.Map({a: 7, b: 8});
someLongComputeFunction(myObj);
<script src="https://cdnjs.cloudflare.com/ajax/libs/immutable/3.8.1/immutable.min.js"></script>
Quelques points à noter:
Les cartes faibles peuvent être utilisées pour stocker des métadonnées sur les éléments du DOM sans interférer avec le nettoyage des ordures ni rendre les collègues en colère contre votre code. Par exemple, vous pouvez les utiliser pour indexer numériquement tous les éléments d’une page Web.
var elements = document.getElementsByTagName('*'),
i = -1, len = elements.length;
while (++i !== len) {
// Production code written this poorly makes me want to cry:
elements[i].lookupindex = i;
elements[i].elementref = [];
elements[i].elementref.Push( elements[Math.pow(i, 2) % len] );
}
// Then, you can access the lookupindex's
// For those of you new to javascirpt, I hope the comments below help explain
// how the ternary operator (?:) works like an inline if-statement
document.write(document.body.lookupindex + '<br />' + (
(document.body.elementref.indexOf(document.currentScript) !== -1)
? // if(document.body.elementref.indexOf(document.currentScript) !== -1){
"true"
: // } else {
"false"
) // }
);
var DOMref = new WeakMap(),
__DOMref_value = Array,
__DOMref_lookupindex = 0,
__DOMref_otherelement = 1,
elements = document.getElementsByTagName('*'),
i = -1, len = elements.length, cur;
while (++i !== len) {
// Production code written this greatly makes me want to ????:
cur = DOMref.get(elements[i]);
if (cur === undefined)
DOMref.set(elements[i], cur = new __DOMref_value)
cur[__DOMref_lookupindex] = i;
cur[__DOMref_otherelement] = new WeakSet();
cur[__DOMref_otherelement].add( elements[Math.pow(i, 2) % len] );
}
// Then, you can access the lookupindex's
cur = DOMref.get(document.body)
document.write(cur[__DOMref_lookupindex] + '<br />' + (
cur[__DOMref_otherelement].has(document.currentScript)
? // if(cur[__DOMref_otherelement].has(document.currentScript)){
"true"
: // } else {
"false"
) // }
);
La différence peut paraître négligeable, mis à part le fait que la version faible est plus longue, mais il existe une différence majeure entre les deux éléments de code présentés ci-dessus. Dans le premier extrait de code, sans mappes faibles, le morceau de code stocke les références dans tous les sens entre les éléments DOM. Cela empêche les éléments DOM d'être collectés. Math.pow(i, 2) % len]
peut sembler être une chose bizarre que personne ne voudrait utiliser, mais détrompez-vous: beaucoup de code de production contient des références DOM qui rebondissent dans tout le document. Maintenant, pour le deuxième morceau de code, parce que toutes les références aux éléments sont faibles, lorsque vous supprimez un nœud, le navigateur est en mesure de déterminer que le nœud n’est pas utilisé (votre code ne peut l’atteindre), et supprimez-le donc de la mémoire. La raison pour laquelle vous devriez vous préoccuper de l’utilisation de la mémoire et des ancres de mémoire (comme le premier fragment de code dans lequel les éléments inutilisés sont conservés en mémoire) tient au fait que plus d’utilisation de la mémoire signifie plus de tentatives de GC dans le navigateur (pour essayer de libérer de la mémoire). éviter une panne du navigateur) signifie une expérience de navigation plus lente et parfois une panne du navigateur.
En ce qui concerne un polyfill pour ceux-ci, je recommanderais ma propre bibliothèque ( trouvé ici @ github ). C'est une bibliothèque très légère qui va simplement la polyfiller sans aucun des frameworks trop complexes que vous pourriez trouver dans d'autres polyfills.
~ Joyeux codage!