web-dev-qa-db-fra.com

Pourquoi l'immuabilité est-elle si importante (ou nécessaire) en JavaScript?

Je travaille actuellement sur React JS et React Native . Sur la route à mi-chemin, je suis tombé sur Immutability ou la bibliothèque Immutable-JS , lorsque je lisais des informations sur l'implémentation de Flux et Redux de Facebook.

La question est de savoir pourquoi l’immuabilité est-elle si importante? Quel est le problème dans les objets en mutation? Cela ne rend-il pas les choses simples?

En donnant un exemple, considérons une application simple News Reader , l’écran d’ouverture constituant une vue en liste des titres.

Si je définis, disons un tableau d'objets avec une valeur initialement je peux pas le manipuler. C'est ce que dit le principe d'immutabilité, non? (Corrigez-moi si je me trompe.) Mais que se passe-t-il si j'ai un nouvel objet News à mettre à jour? Dans le cas habituel, j'aurais pu simplement ajouter l'objet au tableau. Comment puis-je atteindre dans ce cas? Supprimer le magasin et le recréer? L'ajout d'un objet au tableau n'est-il pas une opération moins coûteuse?

169
bozzmob

J'ai récemment fait des recherches sur le même sujet. Je ferai de mon mieux pour répondre à votre question et essayer de partager ce que j'ai appris jusqu'à présent.

La question est de savoir pourquoi l’immuabilité est-elle si importante? Quel est le problème dans les objets en mutation? Cela ne rend-il pas les choses simples?

En gros, l’immuabilité augmente la prévisibilité, la performance (indirectement) et permet le suivi des mutations.

Prévisibilité

La mutation masque le changement, ce qui crée des effets secondaires (inattendus), ce qui peut causer de vilains bugs. Lorsque vous imposez l'immuabilité, vous pouvez garder votre architecture d'application et votre modèle mental simples, ce qui facilite la raisonnement de votre application.

Performance

Même si l'ajout de valeurs à un objet immuable signifie qu'une nouvelle instance doit être créée et que des valeurs existantes doivent être copiées et que de nouvelles valeurs doivent être ajoutées au nouvel objet qui coûte de la mémoire, les objets immuables peuvent utiliser le partage structurel pour réduire la mémoire. aérien.

Toutes les mises à jour renvoient de nouvelles valeurs, mais les structures internes sont partagées afin de réduire considérablement l'utilisation de la mémoire (et les ralentissements du GC). Cela signifie que si vous ajoutez un vecteur de 1 000 éléments, il ne crée pas de nouveau vecteur de 1001 éléments de long. Très probablement, en interne, seuls quelques petits objets sont alloués.

Vous pouvez en lire plus à ce sujet ici .

Suivi des mutations

Outre l'utilisation réduite de la mémoire, l'immuabilité vous permet d'optimiser votre application en utilisant l'égalité de référence et de valeur. Cela rend vraiment facile de voir si quelque chose a changé. Par exemple, un changement d'état dans un composant de réaction. Vous pouvez utiliser shouldComponentUpdate pour vérifier si l'état est identique en comparant les objets d'état et éviter les rendus inutiles. Vous pouvez en lire plus à ce sujet ici .

Ressources additionnelles:

Si je définis dis un tableau d'objets avec une valeur initialement. Je ne peux pas le manipuler. C'est ce que dit le principe d'immutabilité, non? (Corrigez-moi si je me trompe). Mais que se passe-t-il si j'ai un nouvel objet News à mettre à jour? Dans le cas habituel, j'aurais pu simplement ajouter l'objet au tableau. Comment puis-je atteindre dans ce cas? Supprimer le magasin et le recréer? L'ajout d'un objet au tableau n'est-il pas une opération moins coûteuse?

Oui c'est correct. Si vous ne savez pas comment implémenter cela dans votre application, je vous recommanderais de regarder comment redux le fait pour se familiariser avec les concepts de base, cela m'a beaucoup aidé.

J'aime utiliser l'exemple de Redux, car il englobe l'immutabilité. Il possède un seul arbre d'état immuable (appelé store) où tous les changements d'état sont explicites en envoyant des actions qui sont traitées par un réducteur qui accepte l'état précédent avec les actions en question (un à la fois) et renvoie le résultat. prochain état de votre application. Vous pouvez en savoir plus sur ses principes fondamentaux ici .

Il y a un excellent cours de redux sur egghead.ioDan Abramov , l'auteur de redux, explique ces principes comme suit (j'ai légèrement modifié le code pour l'adapter au scénario ):

import React from 'react';
import ReactDOM from 'react-dom';

// Reducer.
const news = (state=[], action) => {
  switch(action.type) {
    case 'ADD_NEWS_ITEM': {
      return [ ...state, action.newsItem ];
    }
    default: {
        return state;
    }
  }
};

// Store.
const createStore = (reducer) => {
  let state;
  let listeners = [];

  const subscribe = (listener) => {
    listeners.Push(listener);

    return () => {
      listeners = listeners.filter(cb => cb !== listener);
    };
  };

  const getState = () => state;

  const dispatch = (action) => {
    state = reducer(state, action);
    listeners.forEach( cb => cb() );
  };

  dispatch({});

  return { subscribe, getState, dispatch };
};

// Initialize store with reducer.
const store = createStore(news);

// Component.
const News = React.createClass({
  onAddNewsItem() {
    const { newsTitle } = this.refs;

    store.dispatch({
      type: 'ADD_NEWS_ITEM',
      newsItem: { title: newsTitle.value }
    });
  },

  render() {
    const { news } = this.props;

    return (
      <div>
        <input ref="newsTitle" />
        <button onClick={ this.onAddNewsItem }>add</button>
        <ul>
          { news.map( ({ title }) => <li>{ title }</li>) }
        </ul>
      </div>
    );
  }
});

// Handler that will execute when the store dispatches.
const render = () => {
  ReactDOM.render(
    <News news={ store.getState() } />,
    document.getElementById('news')
  );
};

// Entry point.
store.subscribe(render);
render();

En outre, ces vidéos montrent plus en détail comment atteindre l'immuabilité pour:

166
danillouz

La question est de savoir pourquoi l’immuabilité est-elle si importante? Quel est le problème dans les objets en mutation? Cela ne rend-il pas les choses simples?

En réalité, le contraire est vrai: la mutabilité rend les choses plus compliquées, du moins à long terme. Oui, cela facilite votre codage initial car vous pouvez simplement changer les choses où vous voulez, mais quand votre programme s'agrandit, cela devient un problème - si une valeur change, qu'est-ce qui le change?

Lorsque vous rendez tout immuable, cela signifie que les données ne peuvent plus être modifiées par surprise. Vous savez avec certitude que si vous transmettez une valeur à une fonction, elle ne peut pas être modifiée dans cette fonction.

En termes simples: si vous utilisez des valeurs immuables, il est très facile de raisonner sur votre code: tout le monde reçoit une copie * unique de vos données, de sorte qu'il ne peut pas être utilisé avec cela et casser d'autres parties de votre code. Imaginez à quel point cela facilite le travail dans un environnement multithread!

Remarque 1: Il existe un coût de performance potentiel lié à l'immutabilité selon ce que vous faites, mais des choses comme Immutable.js optimisent du mieux qu'elles peuvent.

Remarque 2: Dans le cas peu probable où vous ne le sauriez pas, Immutable.js et ES6 const ont une signification très différente.

Dans le cas habituel, j'aurais pu simplement ajouter l'objet au tableau. Comment puis-je atteindre dans ce cas? Supprimer le magasin et le recréer? L'ajout d'un objet au tableau n'est-il pas une opération moins coûteuse? PS: Si l’exemple n’est pas la bonne façon d’expliquer l’immuabilité, faites-le moi savoir quel est le bon exemple concret.

Oui, votre exemple d'actualité est parfaitement bon et votre raisonnement est tout à fait correct: vous ne pouvez pas modifier votre liste existante, vous devez donc en créer une nouvelle:

var originalItems = Immutable.List.of(1, 2, 3);
var newItems = originalItems.Push(4, 5, 6);
49
TwoStraws

Bien que les autres réponses soient correctes, pour répondre à votre question sur un cas d'utilisation pratique (à partir des commentaires des autres réponses), sortons de votre code d'exécution pendant une minute et regardons la réponse omniprésente sous votre nez: git . Que se passerait-il si à chaque fois que vous poussiez un commit, vous écrasiez les données du référentiel?

Nous sommes maintenant confrontés à l'un des problèmes auxquels sont confrontées les collections immuables: la mémoire bouffée. Git est assez intelligent pour ne pas simplement faire de nouvelles copies de fichiers chaque fois que vous apportez une modification, , il garde simplement une trace des diffs .

Bien que je connaisse mal le fonctionnement interne de git, je ne peux que supposer qu'il utilise une stratégie similaire à celle des bibliothèques que vous avez mentionnées: le partage structurel. Sous le capot, les bibliothèques utilisent try ou d’autres arbres pour suivre uniquement les nœuds différents.

Cette stratégie est également raisonnablement performante pour les structures de données en mémoire car il existe bien connus algorithmes de fonctionnement en arborescence qui fonctionnent en temps logarithmique.

Un autre cas d'utilisation: vous voulez un bouton d'annulation sur votre application Web. Avec des représentations immuables de vos données, leur mise en œuvre est relativement simple. Mais si vous comptez sur la mutation, cela signifie que vous devez vous soucier de la mise en cache de l'état du monde et de la mise à jour atomique.

En bref, il y a un prix à payer pour l'immuabilité des performances d'exécution et de la courbe d'apprentissage. Mais tout programmeur expérimenté vous dira que le temps de débogage est plus important que le temps d’écriture de code. Et le léger impact sur les performances d'exécution est probablement compensé par les bugs liés à l'état que vos utilisateurs ne doivent pas endurer.

35
Jared Smith

Pourquoi l'immuabilité est-elle si importante (ou nécessaire) en JavaScript?

L'immuabilité peut être suivie dans différents contextes, mais le plus important serait de la suivre par rapport à l'état de l'application et à l'interface utilisateur de l'application.

Je vais considérer le modèle JavaScript Redux comme une approche très à la mode et moderne et parce que vous en avez parlé.

Pour l'interface utilisateur, nous devons le rendre prévisible . Il sera prévisible si UI = f(application state).

Les applications (en JavaScript) modifient l'état via des actions mises en œuvre à l'aide de la fonction de réduction .

La fonction de réduction prend simplement l'action et l'ancien état et renvoie le nouvel état, en conservant l'ancien.

new state  = r(current state, action)

enter image description here

L'avantage est le suivant: vous parcourez les états dans le temps puisque tous les objets d'état sont enregistrés et vous pouvez rendre l'application dans n'importe quel état depuis UI = f(state).

Ainsi, vous pouvez annuler/rétablir facilement.


Arriver à créer tous ces états peut encore être efficace en mémoire, une analogie avec Git est excellente, et nous avons la même analogie sous Linux avec des liens symboliques (basés sur les inodes).

6
prosti

La question est de savoir pourquoi l’immuabilité est-elle si importante? Quel est le problème dans les objets en mutation? Cela ne rend-il pas les choses simples?

A propos de la mutabilité

Du point de vue technique, rien n’est mal en mutabilité. C'est rapide, c'est réutiliser la mémoire. Les développeurs y sont habitués depuis le début (si je me souviens bien). Il existe un problème d'utilisation de la mutabilité et des problèmes que cette utilisation peut entraîner.

Si l'objet n'est pas partagé avec quoi que ce soit, s'il existe par exemple dans l'étendue de la fonction et n'est pas exposé à l'extérieur, il est difficile de voir les avantages de l'immutabilité. En réalité, dans ce cas, il ne sert à rien d'être immuable. Le sentiment d'immuabilité commence quand quelque chose est partagé.

Céphalée par la mutabilité

Une structure partagée modifiable peut facilement créer de nombreux pièges. Toute modification d'une partie du code donnant accès à la référence a un impact sur les autres parties offrant une visibilité sur cette référence. Un tel impact relie toutes les parties entre elles, même lorsqu'elles ne devraient pas être au courant de différents modules. Une mutation dans une fonction peut planter une partie totalement différente de l'application. Une telle chose est un mauvais effet secondaire.

Ensuite, le problème de mutation est souvent l’état corrompu. Un état corrompu peut se produire lorsque la procédure de mutation échoue au milieu et que certains champs ont été modifiés et d'autres non.

De plus, avec la mutation, il est difficile de suivre le changement. La vérification de référence simple ne montrera pas la différence, pour savoir ce qui a changé, une vérification approfondie doit être effectuée. De plus, pour surveiller le changement, il faut introduire un schéma observable.

Enfin, la mutation est la raison du déficit de confiance. Comment être sûr que certaines structures ont une valeur voulue, si elles peuvent être mutées.

const car = { brand: 'Ferrari' };
doSomething(car);
console.log(car); // { brand: 'Fiat' }

Comme le montre l'exemple ci-dessus, le passage d'une structure mutable peut toujours se terminer par une structure différente. Fonction doQuelque chose est en train de muter l'attribut donné de l'extérieur. Aucune confiance pour le code, vous ne savez vraiment pas ce que vous avez et ce que vous aurez. Tous ces problèmes ont lieu pour les raisons suivantes: Les structures mutables représentent des pointeurs vers la mémoire.

L'immuabilité concerne les valeurs

Immutabilité signifie que le changement n'est pas effectué sur le même objet, la même structure, mais que le changement est représenté dans le nouvel objet. Et c'est parce que la référence représente la valeur et pas seulement le pointeur de la mémoire. Chaque changement crée une nouvelle valeur et ne touche pas l’ancien. Ces règles claires rendent la confiance et la prévisibilité du code. Les fonctions sont sûres à utiliser car au lieu de mutation, elles traitent leurs propres versions avec leurs propres valeurs.

L'utilisation de valeurs au lieu de conteneurs de mémoire donne la certitude que chaque objet représente une valeur non modifiable spécifique et qu'il est sûr de l'utiliser.

Les structures immuables représentent des valeurs.

Je plonge encore plus dans le sujet dans l'article moyen - https://medium.com/@macsikora/the-state-of-immutability-169d2cd1131

6
Maciej Sikora

Un autre avantage de Immutability en Javascript est qu’il réduit le couplage temporel, ce qui présente des avantages substantiels pour la conception en général. Considérons l'interface d'un objet avec deux méthodes:

class Foo {

      baz() {
          // .... 
      }

      bar() {
          // ....
      }

}

const f = new Foo();

Il peut arriver qu'un appel à baz() soit nécessaire pour que l'objet soit dans un état valide pour qu'un appel à bar() fonctionne correctement. Mais comment savez-vous cela?

f.baz();
f.bar(); // this is ok

f.bar();
f.baz(); // this blows up

Pour le comprendre, vous devez examiner les éléments internes de la classe, car l'examen de l'interface publique ne permet pas de déceler immédiatement. Ce problème peut exploser dans une grande base de code comportant de nombreux états et classes mutables.

Si Foo est immuable, le problème ne se pose plus. Il est prudent de supposer que nous pouvons appeler baz ou bar dans n'importe quel ordre, car l'état interne de la classe ne peut pas changer.

4
ghostypants

Une prise différente ...

Mon autre réponse aborde la question d'un point de vue très pratique et je l'aime toujours. J'ai décidé d'ajouter ceci comme une autre réponse plutôt que comme un addendum à celui-ci parce que c'est un discours philosophique ennuyeux qui, espérons-le, répond également à la question, mais ne correspond pas vraiment à ma réponse existante.

TL; DR

Même dans de petits projets, l'immuabilité peut être utile, mais ne supposez pas que, parce qu'elle existe, elle est faite pour vous.

Réponse beaucoup plus longue

NOTE: Aux fins de cette réponse, j'utilise le mot "discipline" pour signifier le renoncement à soi-même pour un avantage quelconque.

Cela ressemble à une autre question: "Devrais-je utiliser du typogramme? Pourquoi les types sont-ils si importants en JavaScript?". Il a une réponse similaire aussi. Considérez le scénario suivant:

Vous êtes l'unique auteur et mainteneur d'une base de code JavaScript/CSS/HTML de quelque 5000 lignes. Votre chef semi-technique lit quelque chose à propos de TypeScript-comme-le-nouveau-hotness et suggère que nous pourrions vouloir passer à cela, mais vous laisse la décision. Donc, vous lisez à ce sujet, jouez avec, etc.

Alors maintenant, vous avez le choix, passez-vous à Typescript?

TypeScript présente des avantages indéniables: intellisense, la détection précoce des erreurs, la spécification immédiate de vos API, la résolution des problèmes de refactoring, le nombre réduit de tests. TypeScript a aussi des coûts: certains idiomes JavaScript très naturels et corrects peuvent être délicats à modéliser dans un système de types pas particulièrement puissant, des annotations augmentent la LoC, le temps et l’effort de réécriture de la base de code existante, une étape supplémentaire dans la construction, etc. Plus fondamentalement, il crée un sous-ensemble de programmes JavaScript corrects possibles en échange de la promesse que votre code est plus probable . C'est arbitrairement restrictif. C’est l’essentiel: vous imposez une discipline qui vous limite (dans l’espoir de vous tirer une balle dans le pied).

Revenons à la question, reformulée dans le contexte du paragraphe ci-dessus: est-ce que en vaut la peine ?

Dans le scénario décrit, je dirais que si vous êtes très familier avec une base de code JS petite à moyenne, le choix d'utiliser TypeScript est plus esthétique que pratique. Et c'est bien , il n'y a rien mal avec l'esthétique, ils ne sont tout simplement pas forcément convaincants.

Scénario B:

Vous changez de poste et vous êtes maintenant programmeur pour Foo Corp. Vous travaillez avec une équipe de 10 personnes sur une base de code JavaScript/HTML/CSS de 90000 LoC (et cela continue de compter) avec un pipeline de construction assez complexe impliquant babel, webpack , une suite de polyfill, réagit avec divers plugins, un système de gestion d’état, environ 20 bibliothèques tierces, ~ 10 bibliothèques internes, des plugins d’éditeur comme un linter avec des règles pour le guide de style interne, etc.

À l'époque où vous étiez une fille/un gars de 5k LoC, cela importait peu. Même la documentation n'était pas si importante, et même en revenant à une partie particulière du code après 6 mois, vous pouviez le comprendre assez facilement. Mais maintenant, la discipline n'est pas seulement agréable, mais est nécessaire . Cette discipline peut ne pas impliquer TypeScript, mais impliquera probablement une forme d'analyse statique ainsi que toutes les autres formes de discipline de codage (documentation, guide de style, scripts de construction, tests de régression, etc.). CI). La discipline n’est plus un luxe , c’est une nécessité .

Tout cela s’appliquait à GOTO en 1978: votre dinky petit jeu de blackjack en C pouvait utiliser GOTOs et la logique spaghetti et il n’était pas si difficile de choisir votre propre aventure à votre façon à travers elle, mais à mesure que les programmes prenaient de l'ampleur et devenaient plus ambitieux, eh bien, indiscipliné , l'utilisation de GOTO ne pouvait être maintenue. Et tout cela s'applique à l'immuabilité aujourd'hui.

Tout comme les types statiques, si vous ne travaillez pas sur une base de code volumineuse avec une équipe d'ingénieurs qui la maintient/l'étend, le choix d'utiliser l'immutabilité est plus esthétique que pratique: ses avantages sont toujours là mais ne peuvent pas dépasser les coûts.

Mais comme pour toutes les disciplines utiles, il arrive un moment où cela n’est plus facultatif. Si je veux maintenir un poids santé, la discipline impliquant la crème glacée peut être facultative. Mais si je veux être un athlète de compétition, mon choix de manger ou non de la crème glacée est subordonné à mon choix d'objectifs. Si vous voulez changer le monde avec un logiciel, l'immutabilité peut faire partie de ce dont vous avez besoin pour éviter qu'elle s'effondre sous son propre poids.

3
Jared Smith

Il était une fois un problème de synchronisation des données entre les threads. Ce problème était très pénible, il y avait plus de 10 solutions. Certaines personnes ont essayé de le résoudre radicalement. C'était un endroit où la programmation fonctionnelle était née. C'est comme le marxisme. Je ne comprenais pas comment Dan Abramov avait vendu cette idée à JS, car elle ne comporte qu'un seul thread. C'est un génie.

Je peux donner un petit exemple. Il existe un attribut __attribute__((pure)) dans gcc. Les compilateurs essaient de déterminer si votre fonction est pure ou non si vous ne la supprimez pas spécialement. Votre fonction peut être pure même si votre état est mutable. L'immuabilité n'est qu'un des 100 moyens de garantir que votre fonction sera pure. En réalité, 95% de vos fonctions seront pures.

Vous ne devriez utiliser aucune limitation (comme l'immutabilité) si vous n'avez pas de raison sérieuse. Si vous voulez "annuler" un état, vous pouvez créer des transactions. Si vous souhaitez simplifier les communications, vous pouvez envoyer des événements avec des données immuables. C'est comme tu veux.

J'écris ce message de la République post marxiste. Je suis sûr que la radicalisation de toute idée est une mauvaise façon.

3
puchu