Maison > interface Web > js tutoriel > le corps du texte

Dérivations mutables en réactivité

Linda Hamilton
Libérer: 2024-10-24 08:22:02
original
814 Les gens l'ont consulté

Toute cette exploration de la planification et de l'async m'a fait réaliser à quel point nous ne comprenons toujours pas la réactivité. Une grande partie de la recherche provient de la modélisation de circuits et d’autres systèmes en temps réel. De nombreuses explorations ont également été réalisées dans les paradigmes de programmation fonctionnelle. Je pense que cela a largement façonné la perspective moderne que nous avons sur la réactivité.

Quand j'ai vu pour la première fois Svelte 3, et plus tard le compilateur React, les gens ont contesté le fait que ces frameworks étaient très précis dans leur rendu. Et pour être honnête, ils partagent en grande partie les mêmes caractéristiques. Si nous devions terminer l'histoire avec les signaux et les primitives dérivées que nous avons vues jusqu'à présent, vous pourriez argumenter sur l'équivalence, sauf que ces systèmes ne permettent pas à leur réactivité de vivre en dehors de leurs composants d'interface utilisateur.

Mais il y a une raison pour laquelle Solid n'a jamais eu besoin d'un compilateur pour accomplir cela. Et pourquoi, à ce jour, c'est encore plus optimal. Ce n'est pas un détail d'implémentation. C'est architectural. C'est lié à l'indépendance réactive par rapport aux composants de l'interface utilisateur, mais c'est bien plus que cela.


Mutable vs Immuable

Dans la définition, la capacité de changer ou de ne pas changer. Mais ce n'est pas ce que nous voulons dire. Nous aurions un logiciel plutôt ennuyeux si rien ne changeait. En programmation, il s'agit de savoir si une valeur peut être muté. Si une valeur ne peut pas être muté, la seule façon de modifier la valeur d'une variable est de la réaffecter.

De ce point de vue, les signaux sont immuables par conception. La seule façon pour eux de savoir si quelque chose a changé est d'intercepter le moment où la nouvelle valeur est attribuée. Si quelqu'un devait muter sa valeur de manière indépendante, rien de réactif ne se produirait.

const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Notre système réactif est un graphe connecté de nœuds immuables. Lorsque nous dérivons des données, nous renvoyons la valeur suivante. Il pourrait même être calculé en utilisant la valeur précédente.

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Mais quelque chose d'intéressant se produit lorsque nous mettons des signaux dans Signaux et des effets dans Effets.

function User(user) {
  // make "name" a Signal
  const [name, setName] = createSignal(user.name);
  return { ...user, name, setName };
}

const [user, setUser] = createSignal(
  new User({ id: 1, name: "John" })
);

createEffect(() => {
  const u = user();
  console.log("User", u.id);
  createEffect(() => {
     console.log("Name", u.name());
  })
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser(new User({ id: 2, name: "Jack" })); 

// effect logs "Name Janet"
user().setName("Janet");
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Maintenant, nous ne pouvons pas seulement changer l'utilisateur mais aussi changer le nom d'un utilisateur. Plus important encore, nous pouvons éviter de faire des travaux inutiles lorsque seul le nom change. Nous ne réexécutons pas l'effet externe. Ce comportement n'est pas une conséquence de l'endroit où l'état est déclaré mais de l'endroit où il est utilisé.

C'est incroyablement puissant, mais il est difficile de dire que notre système est immuable. Oui, l'atome individuel l'est, mais en les imbriquant, nous avons créé une structure optimisée pour la mutation. Seule la partie exacte du code qui doit être exécutée s'exécute lorsque nous modifions quelque chose.

Nous pourrions obtenir les mêmes résultats sans imbrication, mais nous le ferions en exécutant du code supplémentaire pour comparer :

const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Il y a ici un compromis assez clair. Notre version imbriquée devait cartographier les données entrantes pour générer les signaux imbriqués, puis les cartographier une seconde fois pour séparer la manière dont les données étaient accessibles dans des effets indépendants. Notre version diff pourrait utiliser les données brutes mais doit réexécuter tout le code en cas de changement et comparer toutes les valeurs pour déterminer ce qui a changé.

Étant donné que la comparaison est assez performante et que le mappage sur des données particulièrement profondément imbriquées est fastidieux, les gens ont généralement opté pour cette dernière solution. React est fondamentalement ceci. Cependant, les différences ne feront que devenir plus coûteuses à mesure que nos données et le travail lié à ces données augmentent.

Une fois résigné à différer, c'est inévitable. Nous perdons des informations. Vous pouvez le voir dans les exemples ci-dessus. Lorsque nous définissons le nom sur "Janet" dans le premier exemple, nous demandons au programme de mettre à jour le nom user().setName("Janet"). Dans la deuxième mise à jour, nous définissons un tout nouvel utilisateur et le programme doit comprendre ce qui a changé partout où l'utilisateur est consommé.

Bien que l'imbrication soit plus lourde, elle n'exécutera jamais de code inutile. Ce qui m'a inspiré à créer Solid, c'est de réaliser que le plus gros problème lié à la cartographie de la réactivité imbriquée pouvait être résolu avec des proxys. Et les Stores réactifs sont nés :

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Beaucoup mieux.

La raison pour laquelle cela fonctionne est que nous savons toujours que le nom est mis à jour lorsque nous définissonsUser(user => user.name = "Janet"). Le setter pour la propriété name est atteint. Nous réalisons cette mise à jour granulaire, sans mappage sur nos données ni différence.

Pourquoi est-ce important ? Imaginez si vous aviez plutôt une liste d’utilisateurs. Envisagez un changement immuable :

function User(user) {
  // make "name" a Signal
  const [name, setName] = createSignal(user.name);
  return { ...user, name, setName };
}

const [user, setUser] = createSignal(
  new User({ id: 1, name: "John" })
);

createEffect(() => {
  const u = user();
  console.log("User", u.id);
  createEffect(() => {
     console.log("Name", u.name());
  })
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser(new User({ id: 2, name: "Jack" })); 

// effect logs "Name Janet"
user().setName("Janet");
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Nous obtenons un nouveau tableau avec tous les objets utilisateur existants, à l'exception du nouveau avec le nom mis à jour. Tout ce que le cadre sait à ce stade, c'est que la liste a changé. Il devra parcourir toute la liste pour déterminer si des lignes doivent être déplacées, ajoutées ou supprimées, ou si une ligne a changé. S'il a changé, il réexécutera la fonction map et générera la sortie qui sera remplacée/différée par rapport à ce qui se trouve actuellement dans le DOM.

Envisagez un changement mutable :

const [user, setUser] = createSignal({ id: 1, name: "John" });

let prev;
createEffect(() => {
  const u = user();
  // diff values
  if (u.id !== prev?.id) console.log("User", u.id);
  if (u.name !== prev?.name) console.log("Name", u.name);

  // set previous
  prev = u;
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser({ id: 2, name: "Jack" }); 

// effect logs "Name Janet"
setUser({ id: 2, name: "Janet" });
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Nous ne rendons rien. Au lieu de cela, le signal qui correspond au nom de cet utilisateur est mis à jour et exécute l'effet spécifique qui met à jour l'endroit où nous montrons le nom. Pas de récréation de liste. Aucune liste différente. Pas de récréation en ligne. Aucune différence DOM.

En traitant la réactivité mutable comme un citoyen de première classe, nous obtenons une expérience de création similaire à l'état immuable, mais avec des capacités que même le compilateur le plus intelligent ne peut atteindre. Mais nous ne sommes pas ici aujourd’hui pour parler exactement des Stores réactifs. Qu'est-ce que cela a à voir avec les dérivations ?


Revisiter les dérivations

Les valeurs dérivées telles que nous les connaissons sont immuables. Nous avons une fonction qui, chaque fois qu'elle s'exécute, renvoie l'état suivant.

état = fn(état)

Lorsque l'entrée change, elles sont réexécutées et vous obtenez la valeur suivante. Ils remplissent plusieurs rôles importants dans notre graphique réactif.

Premièrement, ils servent de point de mémorisation. Nous pouvons économiser du travail sur des calculs coûteux ou asynchrones si nous reconnaissons que les entrées n'ont pas changé. On peut utiliser une valeur plusieurs fois sans la recalculer.

Deuxièmement, ils agissent comme des nœuds de convergence. Ce sont les « jointures » dans notre graphique. Ils relient plusieurs sources différentes ensemble, définissant leur relation. C'est la clé pour que les choses se mettent à jour ensemble, mais cela va aussi de soi qu'avec un nombre fini de sources et un nombre toujours croissant de dépendances entre elles, tout finirait par s'emmêler.

Cela a beaucoup de sens. Avec les structures de données immuables dérivées, vous n'avez que des "jointures" et non des "forks". À mesure que la complexité augmente, vous êtes destiné à fusionner. Il est intéressant de noter que les "magasins" réactifs n'ont pas cette propriété. Les pièces individuelles sont mises à jour indépendamment. Alors, comment appliquer cette réflexion à la dérivation ?

Suivre la forme

Mutable Derivations in Reactivity

Andre Staltz a publié il y a plusieurs années un article étonnant dans lequel il reliait tous les types de primitives réactives/itérables en un seul continuum. Push/pull tous unifiés sous un seul modèle.

J'ai longtemps été inspiré par la pensée systématique qu'Andre a appliquée dans cet article. Et j'ai longtemps eu du mal avec les sujets que j'ai abordés dans cette série. Parfois, comprendre que l’espace de conception existe suffit pour ouvrir la bonne exploration. Parfois, la forme de la solution est tout ce que vous devez comprendre au début.


Par exemple, j'ai réalisé il y a longtemps que si nous voulions éviter la synchronisation pour certaines mises à jour d'état, nous avions besoin d'un moyen de dériver un état inscriptible. Cela est resté dans mon esprit pendant plusieurs années mais j'ai finalement proposé une dérivation inscriptible.

const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion


L'idée est qu'il est toujours réinitialisable à partir de sa source, mais on peut appliquer des mises à jour de courte durée par dessus jusqu'au prochain changement de source. Pourquoi l'utiliser plutôt qu'un effet ?

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion


Parce que, comme expliqué en profondeur dans la première partie de cette série, le Signal ici n'a jamais pu savoir qu'il dépendait de props.field. Cela détruit la cohérence du graphique car nous ne pouvons pas retracer ses dépendances. Intuitivement, je savais que mettre la lecture dans la même primitive déverrouillait cette capacité. En fait, createWritable est aujourd'hui entièrement implémentable dans le monde utilisateur.

<script> // Detect dark theme var iframe = document.getElementById('tweet-1252839841630314497-983'); if (document.body.className.includes('dark-theme')) { iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1252839841630314497&theme=dark" } </script>
const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

C'est juste un signal d'ordre supérieur. Un signal de signaux ou, comme André l'appelait, une combinaison "Getter-getter" et "Getter-setter". Lorsque le fn transmis exécute la dérivation externe (createMemo), il le suit et crée un signal. Chaque fois que ces dépendances changent, un nouveau signal est créé. Cependant, jusqu'à ce qu'il soit remplacé, ce Signal est actif et tout ce qui écoute la fonction getter renvoyée s'abonne à la fois à la dérivation et au Signal conservant la chaîne de dépendances.

Nous avons atterri ici parce que nous avons suivi la forme de la solution. Et au fil du temps, en suivant cette forme, je crois maintenant, comme indiqué à la fin du dernier article, que cette primitive dérivée mutable est moins une dérivation inscriptible mais un signal dérivé.

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Mais nous sommes toujours face à des primitives immuables. Oui, il s'agit d'une dérivation inscriptible, mais le changement de valeur et la notification se produisent toujours en gros.


Le problème des différences

Mutable Derivations in Reactivity

Intuitivement, nous pouvons voir qu’il y a un écart. Je pouvais trouver des exemples de choses que je voulais résoudre mais je ne pouvais jamais atterrir sur une seule primitive pour gérer cet espace. J'ai réalisé qu'une partie du problème réside dans la forme.

D'une part, nous pourrions mettre des valeurs dérivées dans les magasins :

function User(user) {
  // make "name" a Signal
  const [name, setName] = createSignal(user.name);
  return { ...user, name, setName };
}

const [user, setUser] = createSignal(
  new User({ id: 1, name: "John" })
);

createEffect(() => {
  const u = user();
  console.log("User", u.id);
  createEffect(() => {
     console.log("Name", u.name());
  })
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser(new User({ id: 2, name: "Jack" })); 

// effect logs "Name Janet"
user().setName("Janet");
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Mais comment cela peut-il changer de forme de manière dynamique, c'est à dire générer différents getters sans écrire dans le Store ?

D'un autre côté, nous pourrions dériver des formes dynamiques à partir de Stores mais leur sortie ne serait pas un Store.

const [user, setUser] = createSignal({ id: 1, name: "John" });

let prev;
createEffect(() => {
  const u = user();
  // diff values
  if (u.id !== prev?.id) console.log("User", u.id);
  if (u.name !== prev?.name) console.log("Name", u.name);

  // set previous
  prev = u;
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser({ id: 2, name: "Jack" }); 

// effect logs "Name Janet"
setUser({ id: 2, name: "Janet" });
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Si toutes les valeurs dérivées sont créées à partir du passage d'une fonction wrapper qui renvoie la valeur suivante, comment pouvons-nous un jour isoler le changement ? Au mieux, nous pourrions comparer les nouveaux résultats avec les précédents et appliquer des mises à jour granulaires vers l'extérieur. Mais cela supposait que nous voulions toujours faire des différences.

J'ai lu un article de l'équipe Signia sur les calculs incrémentiels implémentant quelque chose comme le composant For de Solid de manière générique. Cependant, au-delà de la logique qui n'est pas plus simple, j'ai remarqué :

  • C'est un seul Signal immuable. Les modifications imbriquées ne peuvent pas se déclencher indépendamment.

  • Chaque nœud de la chaîne doit participer. Chacun doit appliquer sa différence source pour réaliser des valeurs mises à jour et, à l'exception du nœud final, produire sa différence à transmettre.

Lorsque vous traitez des données immuables. Les références seront perdues. Les différences aident à récupérer ces informations, mais vous en payez ensuite le coût sur toute la chaîne. Et dans certains cas, comme avec des données fraîches du serveur, il n'y a pas de références stables. Quelque chose doit "clé" les modèles et cela n'est pas présent dans Immer qui est utilisé dans l'exemple. React a cette capacité.

C'est à ce moment-là que j'ai pensé que cette bibliothèque avait été conçue pour React. Les hypothèses étaient déjà formulées selon lesquelles il y aurait davantage de différences. Une fois que vous vous résignez à la différence, la différence engendre encore plus de différence. C’est la vérité incontournable. Ils avaient créé un système pour éviter les lourdes tâches en répartissant les coûts supplémentaires sur l'ensemble du système.

Mutable Derivations in Reactivity

J'avais l'impression que j'essayais d'être trop intelligent. La « mauvaise » approche, bien que non durable, est indéniablement la plus performante.


La grande théorie unificatrice de la réactivité (à grain fin)

Mutable Derivations in Reactivity

Il n'y a rien de mal à modéliser des choses de manière immuable. Mais il y a un écart.

Alors suivons la forme :

const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Ce qui devient évident, c'est que la fonction dérivée a la même forme que la fonction de définition du signal. Dans les deux cas, vous transmettez la valeur précédente et renvoyez la nouvelle valeur.

Pourquoi ne faisons-nous pas cela avec les magasins ?

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

On peut même faire rentrer les sources dérivées :

function User(user) {
  // make "name" a Signal
  const [name, setName] = createSignal(user.name);
  return { ...user, name, setName };
}

const [user, setUser] = createSignal(
  new User({ id: 1, name: "John" })
);

createEffect(() => {
  const u = user();
  console.log("User", u.id);
  createEffect(() => {
     console.log("Name", u.name());
  })
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser(new User({ id: 2, name: "Jack" })); 

// effect logs "Name Janet"
user().setName("Janet");
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Il y a une symétrie ici. Un changement immuable construit toujours l'état suivant, et un changement mutable mute l'état actuel vers l'état suivant. Les différences non plus. Si la priorité change sur la dérivation immuable (Mémo), alors toute la référence est remplacée et tous les effets secondaires s'exécutent. Si la priorité change sur la dérivation mutable (projection), seules les choses qui écoutent la priorité sont spécifiquement mises à jour.


Explorer les projections

Le changement immuable est cohérent dans ses opérations, car il lui suffit de construire l'état suivant, quels que soient les changements. Mutable peut avoir différentes opérations en fonction du changement. Le changement immuable a toujours l'état précédent non modifié avec lequel travailler, contrairement au changement mutable. Cela a un impact sur les attentes.

On peut le voir dans la section précédente avec la nécessité d'utiliser concilier dans l'exemple. Lorsqu'un tout nouvel objet est transmis avec Projections, vous ne vous contentez pas de tout remplacer. Vous devez appliquer les modifications progressivement. En fonction des mises à jour, il faudra peut-être muter de différentes manières. Vous pouvez appliquer toutes les modifications à chaque fois et tirer parti des contrôles d'égalité internes d'un magasin :

const [user, setUser] = createSignal({ id: 1, name: "John" });

let prev;
createEffect(() => {
  const u = user();
  // diff values
  if (u.id !== prev?.id) console.log("User", u.id);
  if (u.name !== prev?.name) console.log("Name", u.name);

  // set previous
  prev = u;
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser({ id: 2, name: "Jack" }); 

// effect logs "Name Janet"
setUser({ id: 2, name: "Janet" });
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Mais cela devient vite prohibitif car cela ne fonctionne que superficiellement. La réconciliation (la différence) est toujours une option. Mais souvent, ce que nous voulons faire, c’est appliquer uniquement ce dont nous avons besoin. Cela conduit à un code plus compliqué mais peut être beaucoup plus efficace.

En modifiant un extrait du clone Trello de Solid, nous pouvons utiliser une projection pour soit appliquer individuellement chaque mise à jour optimiste, soit réconcilier le tableau avec la dernière mise à jour du serveur.

const [signal, setSignal] = createSignal({ a: 1 });

createEffect(() => console.log(signal().a)); // logs "1"

// does not trigger the effect
signal().a = 2; 

setSignal({ a: 3 }); // the effect logs "3"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

C'est puissant car non seulement il conserve les références dans l'interface utilisateur afin que seules des mises à jour granulaires se produisent, mais il applique également des mutations (mises à jour optimistes) de manière incrémentielle sans clonage ni différence. Ainsi, non seulement les composants n'ont pas besoin d'être réexécutés, mais à chaque modification, il n'est pas nécessaire de reconstruire l'ensemble de l'état de la carte à plusieurs reprises pour se rendre compte une fois de plus que très peu de choses ont changé. Et enfin, lorsqu'il est nécessaire d'effectuer une comparaison, lorsque le serveur renvoie enfin nos nouvelles données, il effectue une comparaison avec cette projection mise à jour. Les références sont conservées et rien n'a besoin d'être restitué.

Bien que je pense que cette approche sera une énorme victoire pour les systèmes en temps réel et locaux à l'avenir, nous utilisons déjà les projections aujourd'hui, peut-être sans nous en rendre compte. Considérez les fonctions de carte réactive qui incluent des signaux pour l'index :

const [log, setLog] = createSignal("start");

const allLogs = createMemo(prev => prev + log()); // derived value
createEffect(() => console.log(allLogs())); // logs "start"

setLog("-end"); // effect logs "start-end"
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

L'index est projeté sur votre liste de lignes qui ne contiennent pas d'index en tant que propriété réactive. Maintenant, cette primitive est tellement de base que je ne l'implémenterai probablement pas avec createProjection mais il est important de comprendre qu'il s'agit d'une primitive catégorique.

Un autre exemple est l'obscure API createSelector de Solid. Il vous permet de projeter l'état de sélection sur une liste de lignes de manière performante afin que la modification de ce qui est sélectionné ne mette pas à jour chaque ligne. Grâce à une primitive de Projection formalisée nous n'avons plus besoin de primitive spéciale :

function User(user) {
  // make "name" a Signal
  const [name, setName] = createSignal(user.name);
  return { ...user, name, setName };
}

const [user, setUser] = createSignal(
  new User({ id: 1, name: "John" })
);

createEffect(() => {
  const u = user();
  console.log("User", u.id);
  createEffect(() => {
     console.log("Name", u.name());
  })
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser(new User({ id: 2, name: "Jack" })); 

// effect logs "Name Janet"
user().setName("Janet");
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cela crée une carte dans laquelle vous recherchez par identifiant mais seule la ligne sélectionnée existe. Puisqu'il s'agit d'un proxy pour la durée de vie de l'abonnement, nous pouvons suivre les propriétés qui n'existent pas et les avertir lorsqu'elles sont mises à jour. La modification du SelectionId mettra à jour au maximum 2 lignes : celle déjà sélectionnée et la nouvelle en cours de sélection. Nous transformons une opération O(n) en O(2).

En jouant davantage avec cette primitive, j'ai réalisé qu'elle accomplissait non seulement une dérivation mutable directe, mais qu'elle pouvait être utilisée pour transmettre la réactivité de manière dynamique.

const [user, setUser] = createSignal({ id: 1, name: "John" });

let prev;
createEffect(() => {
  const u = user();
  // diff values
  if (u.id !== prev?.id) console.log("User", u.id);
  if (u.name !== prev?.name) console.log("Name", u.name);

  // set previous
  prev = u;
}); // logs "User 1", "Name John"

// effect logs "User 2", "Name Jack"
setUser({ id: 2, name: "Jack" }); 

// effect logs "Name Janet"
setUser({ id: 2, name: "Janet" });
Copier après la connexion
Copier après la connexion
Copier après la connexion
Copier après la connexion

Cette projection expose uniquement l'identifiant et le nom de l'utilisateur mais garde privateValue inaccessible. Cela fait quelque chose d'intéressant dans la mesure où il applique un getter au nom. Ainsi, même si la projection se réexécute lorsque nous remplaçons l'ensemble de l'utilisateur, seule la mise à jour du nom de l'utilisateur peut exécuter l'effet sans que la projection ne soit réexécutée.

Ces cas d'utilisation ne sont que quelques-uns et j'avoue qu'il faut un peu plus de temps pour comprendre. Mais je pense que les projections sont le chaînon manquant dans l'histoire des signaux.


Conclusion

Mutable Derivations in Reactivity

Tout au long de cette exploration des dérivations de la réactivité au cours des deux dernières années, j'ai beaucoup appris. Toute ma perspective sur la réactivité a changé. La mutabilité, au lieu d'être considérée comme un mal nécessaire, est devenue en moi un pilier distinct de la réactivité. Quelque chose qui n'est pas imité par les approches ou les compilateurs axés sur les cours.

Il s'agit d'une déclaration puissante qui suggère une différence fondamentale entre la réactivité immuable et mutable. Un signal et un magasin ne sont pas la même chose. Les mémos et les projections non plus. Bien que nous puissions peut-être unifier leurs API, nous ne devrions peut-être pas le faire.

Je suis arrivé à ces réalisations en suivant la forme de l'API de Solid, mais d'autres solutions ont des API différentes pour leurs signaux. Je me demande donc s'ils seraient parvenus aux mêmes conclusions. Pour être honnête, la mise en œuvre de Projections présente des défis et l'histoire n'est pas terminée. Mais je pense que cela met fin à notre exploration de la pensée pour le moment. J'ai beaucoup de travail devant moi.

Merci de m'avoir rejoint dans ce voyage.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

source:dev.to
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal
À propos de nous Clause de non-responsabilité Sitemap
Site Web PHP chinois:Formation PHP en ligne sur le bien-être public,Aidez les apprenants PHP à grandir rapidement!