3 points par GN⁺ 2024-04-01 | 1 commentaires | Partager sur WhatsApp

Ébauche de proposition de standardisation de JavaScript Signals

  • Document décrivant une direction commune initiale pour les signaux (signals) en JavaScript, dans un esprit similaire aux efforts Promises/A+ ayant précédé la standardisation des Promises par le TC39 avec ES2015.
  • Cet effort vise à coordonner l’écosystème JavaScript, et si cette coordination réussit, une norme pourra émerger à partir de cette expérience.
  • Plusieurs auteurs de frameworks collaborent autour d’un modèle commun pouvant servir de socle à un cœur réactif.
  • Le brouillon actuel s’appuie sur des contributions de conception d’auteurs et de mainteneurs d’Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz, entre autres.

Contexte : pourquoi les signals ?

  • Pour développer des interfaces utilisateur (UI) complexes, les développeurs d’applications JavaScript doivent pouvoir stocker, calculer, invalider, synchroniser et propager l’état vers la couche de vue de l’application de manière efficace.
  • Une UI ne se limite pas à gérer des valeurs simples : elle implique souvent l’affichage d’un état calculé dépendant d’autres valeurs ou états.
  • L’objectif des signals est de fournir l’infrastructure nécessaire à la gestion de cet état applicatif afin que les développeurs puissent se concentrer sur la logique métier plutôt que sur des détails répétitifs.
Exemple - compteur en VanillaJS
  • On dispose d’une variable counter, et l’on souhaite mettre à jour dans le DOM la parité du compteur chaque fois que cette variable change.
  • En Vanilla JS, on pourrait avoir un code comme celui-ci :
let counter = 0;
const setCounter = (value) => {
  counter = value;
  render();
};

const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();

// Simulate external updates to counter...
setInterval(() => setCounter(counter + 1), 1000);
  • Ce code présente plusieurs problèmes :
    • l’affectation de counter est verbeuse et pleine de boilerplate ;
    • l’état de counter est étroitement couplé au système de rendu ;
    • lorsque counter change mais que parity ne change pas (par exemple de 2 à 4), des calculs et rendus inutiles sont effectués ;
    • si d’autres parties de l’UI veulent ne se rendre qu’au moment des mises à jour de counter ;
    • d’autres parties de l’UI qui ne dépendent que de isEven ou parity ne peuvent pas être mises à jour sans interagir directement avec counter.

Introduction aux signals

  • Les abstractions de liaison de données entre modèle et vue sont depuis longtemps au cœur des frameworks UI, même si ni JS ni la plateforme web n’intègrent nativement un tel mécanisme.
  • Au sein des frameworks et bibliothèques JS, de nombreuses expérimentations ont été menées sur les différentes manières de représenter ces liaisons, et l’approche consistant à utiliser des valeurs réactives de premier ordre représentant un état ou des calculs dérivés d’autres données, souvent appelées « Signals », a démontré sa puissance.
  • Si l’on réimagine l’exemple ci-dessus avec une API de signals, on obtient ceci :
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");

// A library or framework defines effects based on other Signal primitives
declare function effect(cb: () => void): (() => void);

effect(() => element.innerText = parity.get());

// Simulate external updates to counter...
setInterval(() => counter.set(counter.get() + 1), 1000);

Motivations pour la standardisation des signals

Interopérabilité
  • Chaque implémentation de signals possède son propre mécanisme de suivi automatique, ce qui rend difficile le partage de modèles, de composants et de bibliothèques entre frameworks.
  • Cette proposition vise à séparer complètement le modèle réactif de la vue de rendu, afin que les développeurs n’aient pas à réécrire leur code non-UI lorsqu’ils migrent vers une nouvelle technologie de rendu, ou puissent développer en JS des modèles réactifs partagés déployables dans d’autres contextes.
Performances / utilisation mémoire
  • Le fait d’envoyer moins de code grâce à une bibliothèque couramment utilisée intégrée apporte toujours un petit gain potentiel de performance, mais comme les implémentations de signals sont généralement assez petites, on ne s’attend pas à ce que cet effet soit très important.
Outils pour développeurs
  • Avec les bibliothèques de signals existantes en JS, il est difficile de suivre la pile d’appels à travers une chaîne de signals calculés, le graphe de références entre signals, etc.
  • Des signals intégrés permettraient au runtime JS et aux outils de développement d’offrir un meilleur support pour leur inspection.
Bénéfices annexes
Avantages de la bibliothèque standard
  • Historiquement, JavaScript a eu une bibliothèque standard plutôt minimale, mais la tendance au TC39 est d’en faire un langage « batteries incluses » avec un ensemble de fonctionnalités intégrées de haute qualité.
Intégration HTML/DOM (possibilité future)
  • Le W3C et les implémenteurs de navigateurs travaillent actuellement à l’introduction de templates natifs dans HTML.
  • Pour atteindre cet objectif, HTML aura à terme besoin de primitives réactives.

Objectifs de conception des signals

  • Les bibliothèques de signals existantes ne diffèrent pas énormément sur le fond.
  • Cette proposition cherche à s’appuyer sur leurs réussites en implémentant des caractéristiques importantes communes à de nombreuses bibliothèques.

Fonctionnalités clés

  • Un type Signal représentant l’état, c’est-à-dire un Signal inscriptible.
  • Un type de Signal calculé / mémoïsé / dérivé qui dépend d’autres signals, se calcule paresseusement et est mis en cache.
  • La possibilité pour les frameworks JS de gérer leur propre ordonnancement.

Esquisse d’API

  • L’idée initiale d’API pour les signals est la suivante. Il ne s’agit que d’un premier brouillon, qui devrait évoluer avec le temps.
namespace Signal {
  // A read-write Signal
  class State<T> implements Signal<T> {
    // Create a state Signal starting with the value t
    constructor(t: T, options?: SignalOptions<T>);
    
    // Get the value of the signal
    get(): T;
    
    // Set the state Signal value to t
    set(t: T): void;
  }
  
  // A Signal which is a formula based on other Signals
  class Computed<T> implements Signal<T> {
    // Create a Signal which evaluates to the value returned by the callback.
    // Callback is called with this signal as the this value.
    constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);
    
    // Get the value of the signal
    get(): T;
  }
  
  // This namespace includes "advanced" features that are better to
  // leave for framework authors rather than application developers.
  // Analogous to `crypto.subtle`
  namespace subtle {
    // Run a callback with all tracking disabled (even for nested computed).
    function untrack<T>(cb: () => T): T;
    
    // Get the current computed signal which is tracking any signal reads, if any
    function currentComputed(): Computed | null;
    
    // Returns ordered list of all signals which this one referenced
    // during the last time it was evaluated.
    // For a Watcher, lists the set of signals which it is watching.
    function introspectSources(s: Computed | Watcher): (State | Computed)[];
    
    // Returns the Watchers that this signal is contained in, plus any
    // Computed signals which read this signal last time they were evaluated,
    // if that computed signal is (recursively) watched.
    function introspectSinks(s: State | Computed): (Computed | Watcher)[];
    
    // True if this signal is "live", in that it is watched by a Watcher,
    // or it is read by a Computed signal which is (recursively) live.
    function hasSinks(s: State | Computed): boolean;
    
    // True if this element is "reactive", in that it depends
    // on some other signal. A Computed where hasSources is false
    // will always return the same constant.
    function hasSources(s: Computed | Watcher): boolean;
    
    class Watcher {
      // When a (recursive) source of Watcher is written to, call this callback,
      // if it hasn't already been called since the last `watch` call.
      // No signals may be read or written during the notify.
      constructor(notify: (this: Watcher) => void);
      
      // Add these signals to the Watcher's set, and set the watcher to run its
      // notify callback next time any signal in the set (or one of its dependencies) changes.
      // Can be called with no arguments just to reset the "notified" state, so that
      // the notify callback will be invoked again.
      watch(...s: Signal[]): void;
      
      // Remove these signals from the watched set (e.g., for an effect which is disposed)
      unwatch(...s: Signal[]): void;
      
      // Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
      // with a source which is dirty or pending and hasn't yet been re-evaluated
      getPending(): Signal[];
    }
    
    // Hooks to observe being watched or no longer watched
    var watched: Symbol;
    var unwatched: Symbol;
  }
  
  interface Options<T> {
    // Custom comparison function between old and new value. Default: Object.is.
    // The signal is passed in as the this value for context.
    equals?: (this: Signal<T>, t: T, t2: T) => boolean;
    
    // Callback called when isWatched becomes true, if it was previously false
    [Signal.subtle.watched]?: (this: Signal<T>) => void;
    
    // Callback called whenever isWatched becomes false, if it was previously true
    [Signal.subtle.unwatched]?: (this: Signal<T>) => void;
  }
}

Algorithme des signals

  • Décrit les algorithmes implémentés pour chaque API exposée à JavaScript.
  • On peut y voir une spécification initiale, qui cherche à fixer autant que possible un ensemble de sémantiques malgré des évolutions encore très ouvertes.

Avis de GN⁺

  • La proposition de standardisation de JavaScript Signals vise à améliorer l’interopérabilité entre frameworks et à faciliter la mise en œuvre de la programmation réactive par les développeurs.
  • Cette proposition constitue une tentative de standardiser les fonctionnalités de base de plusieurs bibliothèques de signals existantes, ce qui pourrait offrir aux développeurs un modèle de programmation cohérent.
  • Le concept de signals peut être utile non seulement pour le développement UI, mais aussi dans des contextes non UI, notamment pour éviter des rebuilds inutiles dans les systèmes de build.
  • L’API proposée fournit des outils utiles aux développeurs de frameworks, avec l’espoir d’obtenir de meilleures performances et une meilleure gestion mémoire.
  • Cependant, pour que cette technologie soit largement adoptée, davantage de prototypage et de retours de la communauté seront nécessaires, et son efficacité devra être démontrée dans de vraies applications.
  • Des frameworks comme React, Vue et Svelte disposent déjà de leurs propres systèmes réactifs, et les questions de compatibilité ou de stratégie d’intégration avec ces frameworks constitueront aussi un point important.

1 commentaires

 
GN⁺ 2024-04-01
Avis Hacker News
  • Exemple Vanilla JS vs. Signals

    • Suis-je le seul à trouver l’exemple en Vanilla JS plus lisible et plus agréable à utiliser ?
      • La configuration semble complexe et il y a beaucoup de boilerplate.
      • Quand la valeur du compteur change, cela peut provoquer des calculs et des rendus inutiles.
      • Si d’autres parties de l’UI doivent être rendues uniquement lors des mises à jour du compteur, il faudra peut-être changer la façon de gérer l’état.
      • Si d’autres parties de l’UI dépendent uniquement de isEven ou de parity, il pourrait être nécessaire de revoir toute l’approche.
  • Promises et l’évolution de JavaScript

    • Au début, je me demandais si j’allais devoir utiliser souvent new Promise, mais en pratique je ne m’en suis presque jamais servi.
    • À la place, j’ai beaucoup utilisé .then, ce qui simplifie l’interface avec diverses bibliothèques tierces.
    • Si cette proposition de Signal peut avoir un effet similaire pour les frameworks d’UI réactifs, je suis pour.
  • Signals comme partie intégrante du langage

    • Il n’est pas nécessaire que les Signals fassent partie du langage ; une bibliothèque suffit.
    • Penser que les Signals conçus par les bibliothèques UI actuelles sont assez bons pour faire partie du langage relève de l’arrogance.
    • Ajouter chaque mode au runtime du langage ressemble à une vision à court terme.
  • Utilisation des événements dans les applications

    • J’utilise des événements dans toute l’application pour envoyer des signaux.
    • Je déclenche et j’écoute des événements via window.dispatchEvent et window.addEventListener.
  • La difficulté de la gestion d’état du DOM et des mises à jour

    • J’essaie de comprendre pourquoi, depuis des décennies, les gens trouvent la gestion d’état et les mises à jour du DOM si difficiles.
    • Cela me laisse perplexe, car on dirait qu’on complique de simples fonctions DOM.
  • Promises et programmation asynchrone

    • Les Promises sont un cas de réussite, mais elles n’auraient pas eu besoin d’être standardisées sans async/await.
    • Je me demande ce que pensent de cette proposition les auteurs de différentes bibliothèques.
  • S.js et Signals

    • J’aime les Signals et je les préfère à d’autres primitives pour construire des UI.
    • Mais je ne pense pas qu’ils devraient être intégrés au langage JavaScript.
  • Des Signals proches de MobX

    • MobX est mon système d’effets JS préféré.
    • Exemple de code fourni dans la version MobX.
  • Ajouter un framework à la bibliothèque standard

    • C’est comparable à l’idée d’ajouter son framework préféré du moment à la bibliothèque standard.
  • Compréhension et problèmes de la proposition Signal

    • J’ai du mal à comprendre les exemples de cette proposition Signal.
    • Je me demande comment la fonction effect détecte les changements de parity, et si cette lambda est appelée à chaque modification de signal.
    • L’idée des Signals se tient, mais dans des applications complexes, il peut devenir difficile de suivre les événements.