3 points par GN⁺ 2024-04-01 | 1 commentaires | Partager sur WhatsApp
  • La proposition JavaScript Signals de TC39 est une première orientation visant à standardiser des primitives réactives pour suivre efficacement l’état d’une UI et les états calculés ; elle en est actuellement au stade de brouillon Stage 1
  • La proposition se concentre moins sur une API de surface directement utilisée par les développeurs d’applications que sur la sémantique centrale du graphe de Signals et le mécanisme de suivi automatique que les frameworks pourraient partager
  • Signal.State, Signal.Computed et Signal.subtle.Watcher constituent les API principales ; les calculs visent la lazy evaluation, la mise en cache, le suivi automatique des dépendances et une exécution sans glitch
  • L’objectif des Signals intégrés est d’améliorer l’interopérabilité entre plusieurs frameworks comme Angular, Ember, MobX, Preact, Qwik, Solid, Svelte et Vue, et d’ouvrir la voie à une meilleure prise en charge du débogage et de l’analyse de performances dans les DevTools
  • Le groupe de proposition souhaite, avant le Stage 2, passer par plusieurs polyfills de niveau production, des intégrations dans des frameworks, une validation sur de grandes applications et des benchmarks de performance ; la standardisation pourrait prendre au minimum 2 à 3 ans ou plus

Positionnement de la proposition JavaScript Signals

  • JavaScript Signals est présenté comme une proposition Stage 1 du processus TC39
  • Le document actuel est une tentative d’aligner l’écosystème JavaScript sur une direction commune, à la manière de Promises/A+ avant la standardisation de Promise dans ES2015
  • Un polyfill est disponible pour l’expérimentation directe
  • Les champions de la proposition et les auteurs initiaux ont construit le brouillon actuel à partir des contributions de conception de plusieurs frameworks et bibliothèques

Le problème visé par la standardisation

  • Une UI complexe doit stocker des valeurs, les calculer, les invalider, les synchroniser et les pousser vers la couche de vue ; les Signals cherchent à fournir une infrastructure de gestion d’état pour ces tâches répétitives
  • Dans l’exemple en Vanilla JS, counter, isEven, parity et render s’entremêlent directement, ce qui entraîne les problèmes suivants
    • Le système d’état et le système de rendu sont fortement couplés
    • Des calculs et rendus inutiles se produisent même quand parity ne change pas, par exemple lorsque counter passe de 2 à 4
    • Lorsqu’un autre fragment d’UI veut ne s’abonner qu’à une partie de counter, isEven ou parity, la gestion manuelle des abonnements et désabonnements devient complexe
    • Ajouter du pub/sub à plusieurs niveaux augmente le boilerplate et la bookkeeping des abonnements, avec un risque de fuites mémoire
  • Un exemple fondé sur les Signals utilise Signal.State et Signal.Computed pour traiter les valeurs, les calculs et les effets de bord d’une seule manière
    • Aucun abonnement manuel n’est nécessaire
    • Un Signal calculé détecte automatiquement les Signals dont il dépend
    • Les calculs ne s’exécutent que lorsque leur valeur est explicitement demandée
    • Un Signal calculé met en cache sa dernière valeur

API principales de la proposition

  • Signal<T> est défini comme une valeur lisible dotée de get(): T
  • Signal.State<T> est un Signal inscriptible
    • Le constructeur reçoit une valeur initiale et des options
    • La valeur se lit avec get() et se modifie avec set(t)
  • Signal.Computed<T> est un Signal calculé fondé sur d’autres Signals
    • Il est calculé à partir de la valeur renvoyée par un callback
    • Il suit automatiquement ses dépendances
    • Sa valeur est calculée paresseusement et mise en cache
  • Signal.subtle contient des API avancées davantage destinées aux auteurs de frameworks ou aux implémentations de DevTools
    • untrack(cb) permet de lire un Signal sans suivi
    • currentComputed() renvoie le Signal computed actuellement suivi
    • introspectSources, introspectSinks, hasSinks et hasSources sont des API d’observation du graphe
    • Watcher sert de base pour détecter les changements de Signals et implémenter des effects et de la planification au niveau des frameworks
  • SignalOptions<T> prend en charge une fonction de comparaison personnalisée equals ainsi que les hooks watched / unwatched

Fonctionnement et modèle d’exécution

  • Un Signal représente une cellule de données pouvant changer au fil du temps, classée en state ou computed
  • Un Signal calculé enregistre automatiquement les Signals lus pendant son exécution, puis vérifie, lors d’une lecture ultérieure, si ses anciennes dépendances ont changé
  • Le calcul est pull-based
    • Même si une dépendance change, il n’y a pas de recalcul immédiat
    • Le recalcul n’a lieu si nécessaire que lorsque quelqu’un lit la valeur avec .get()
  • Les écritures dans un State Signal sont reflétées de manière synchrone
    • Après .set(), la lecture d’un computed Signal qui dépend de cette valeur déclenche immédiatement un recalcul si nécessaire
    • Il n’y a pas de batching intégré
  • Le callback notify d’un Watcher peut s’exécuter de façon synchrone pendant un .set()
    • Toutefois, il n’est pas possible de lire ou d’écrire un Signal pendant notify
    • Les opérations réelles de lecture/écriture doivent être planifiées ensuite
  • Si le callback d’un computed Signal lève une exception, cette exception est aussi mise en cache comme une valeur, puis relancée lors de la lecture suivante du Signal

Motivation de la standardisation

  • Chaque implémentation de Signals dans les frameworks possède son propre mécanisme de suivi automatique, ce qui rend difficile le partage de modèles, composants et bibliothèques entre frameworks
  • L’objectif de la proposition est de séparer le modèle réactif de la vue de rendu
    • Permettre aux développeurs de changer de technologie de rendu sans réécrire le code non-UI
    • Permettre de créer en JavaScript des modèles réactifs partageables entre plusieurs contextes
  • Sur les plans performance et mémoire, le document précise qu’une implémentation intégrée pourrait être plus efficace qu’une implémentation JS d’un petit facteur constant, mais que le moteur ne changera pas magiquement les algorithmes
  • Côté DevTools, les Signals intégrés pourraient mieux exposer les informations suivantes
    • La call stack des chaînes de computed Signals
    • Le graphe de références entre Signals
    • Les relations de dépendance nécessaires au débogage de l’utilisation mémoire
  • Une inclusion dans la bibliothèque standard pourrait aussi entraîner des effets secondaires comme une réduction de la taille des bundles, une amélioration de la stabilité et de la qualité, et la formation d’un vocabulaire commun entre projets

Objectifs de conception et contraintes

  • Les fonctionnalités centrales incluent les Signals inscriptibles, les Signals calculés, la réaction à l’état dirty, la planification propre aux frameworks, untrack et la composition de plusieurs bases de code
  • Les Signals calculés visent une exécution sans glitch
    • Pour éviter les calculs inutiles, les parties potentiellement dirty du graphe sont exécutées selon un tri topologique
    • L’objectif est d’éliminer les calculs redondants
  • Aucune planification forcée intégrée de style Promise n’est incluse, afin de permettre aux frameworks d’assurer leur propre scheduling
  • Pour éviter les mauvais usages des callbacks réactifs synchrones, la lecture et l’écriture de Signals sont interdites dans le notify d’un Watcher
  • untrack est traité comme une échappatoire non sûre
    • Si un Signal lu sans suivi influence le résultat d’un calcul, le computed Signal peut ne pas être mis à jour lorsque ce Signal change
  • L’API donne la priorité au fait de servir de base aux implémentations de frameworks, plutôt qu’à une ergonomie spécialement pensée pour les développeurs d’applications généralistes

effect et Watcher

  • La proposition ne contient pas de fonction intégrée comme effect()
  • La planification des effects étant liée au cycle de rendu du framework, à la disposal et à la gestion de la propriété, l’API standard JavaScript ne la résout pas directement
  • À la place, Signal.subtle.Watcher fournit une base de bas niveau pour implémenter des effects
    • Lorsque les dépendances d’un Signal observé changent, notify est appelé
    • getPending() permet de vérifier quels Signals sont encore dirty
    • Les effects nécessitant une disposal doivent être nettoyés avec unwatch
  • Les Signals surveillés par un Watcher peuvent rester vivants tant que leur state interne est accessible ; il faut donc appeler Watcher.prototype.unwatch lors du nettoyage des effects

Fonctionnalités absentes du brouillon actuel

  • Async n’est pas inclus dans le modèle actuel
    • Les Signals sont toujours traités comme des valeurs évaluables de manière synchrone
    • Il est partiellement possible de modéliser l’état loading avec des exceptions ; la discussion sur les améliorations se trouve dans l’issue #30
  • Transactions n’est pas inclus non plus
    • Maintenir simultanément un état “from” et un état “to” lors d’une transition d’écran pose le problème du fork de l’état du graphe de Signals
    • La discussion liée se trouve dans l’issue #73
  • Certaines convenience methods sont également absentes du brouillon actuel
  • Ces fonctionnalités manquantes ont été exclues en raison d’un manque de consensus entre frameworks et parce qu’il est possible de les contourner dans les couches supérieures, mais elles pourront être réexaminées après le prototype

Plan de développement et calendrier de standardisation

  • Cette proposition figure à l’ordre du jour TC39 Stage 1 d’avril 2024, et le document explique qu’elle peut actuellement être vue comme un Stage 0
  • Avant de proposer le Stage 2, les travaux suivants sont prévus
    • Développer plusieurs implémentations de polyfills de niveau production
    • Tester divers frameworks et réussir des tests de style test262
    • Valider les performances au moyen d’un thorough signal/framework benchmark set
    • Intégrer l’API proposée dans plusieurs frameworks JS représentatifs et dans certaines grandes applications
    • Comprendre les possibilités d’extension de l’API et décider de leur inclusion
  • Le groupe de proposition souhaite avancer prudemment afin d’éviter de standardiser trop vite une mauvaise forme de Signals
  • La FAQ estime qu’il faudra au minimum 2 à 3 ans avant que les Signals standard puissent être utilisés dans l’ensemble des navigateurs sans polyfill
  • Le polyfill actuel est utilisable, mais il est recommandé de ne pas dépendre de la stabilité de l’API, car elle peut évoluer pendant le processus d’examen

Modèle d’utilisation résumé dans la FAQ

  • Les Signals intégrés sont indépendants de la technologie de rendu
    • Le document indique que des formes comme Preact avec VDOM, Solid avec DOM natif ou Vue avec une approche hybride sont toutes possibles
  • Pour les développeurs d’applications, il est généralement préférable d’utiliser les Signals via un framework
    • Le framework gère les Watchers, untrack, l’ownership, la disposal et la planification du rendu DOM
  • Ils peuvent être utilisés avec SSR, hydration et resumability
    • Qwik utilise les Signals avec ces propriétés, et la proposition considère que les resumable Signals de Qwik peuvent être modélisés par une combinaison de State et Computed
  • Les Signals et Proxy sont complémentaires
    • Proxy intercepte les opérations superficielles sur les objets, tandis que les Signals coordonnent le graphe de dépendances des cellules de données
    • Placer des Signals derrière Proxy peut rendre une nested reactive structure plus ergonomique
  • Les Signals ne sont pas des flux, mais des cellules représentant la valeur actuelle
    • Si l’on écrit deux fois de suite dans un State Signal sans rien faire d’autre, la première écriture peut ne pas être visible par un computed Signal ou un effect
    • Le document y voit une propriété située à l’opposé de l’exécution sans glitch, et indique que d’autres constructions comme async iterable ou observable conviennent mieux aux flux

1 commentaires

 
GN⁺ 2024-04-01
Commentaires sur Hacker News
  • Suis-je le seul à trouver que les exemples en pur JavaScript sont au contraire plus faciles à lire et à manipuler ?
    On dit que « la configuration ajoute du bruit et beaucoup de boilerplate », mais les exemples avec signals me paraissent tout aussi bruyants et verbeux, tout en ajoutant de nouveaux concepts difficiles à comprendre pour les débutants
    Dire que « si counter passe de 2 à 4, parity ne change pas mais on fait quand même des calculs et des rendus inutiles » ressemble à une mémoïsation prématurée
    Si d'autres parties de l'UI veulent se re-rendre lors des mises à jour de counter, alors oui, cet exemple simpliste n'est pas approprié, et dans ce cas on peut utiliser d'autres approches comme les signals, la gestion d'événements ou un store d'état centralisé (de type Redux)
    Si d'autres parties de l'UI ne dépendent que de isEven ou de parity, on pourrait changer d'approche si c'est au cœur de l'architecture de l'application, mais la plupart du temps ce n'est pas le cas. Le fait que « la fonction de rendu qui ne dépend que de parity doive savoir qu'elle doit s'abonner à counter » n'est pas forcément une charge injuste, et les fonctions de calcul pures ont l'avantage de rendre leurs entrées faciles à identifier

    • Je ne vois pas pourquoi on considère cela comme de la mémoïsation prématurée. Ce n'est qu'un exemple réduit à une fonction simple, et il est difficile de croire que ce cas d'usage a été inventé alors que personne n'en avait réellement besoin
      Tenter de standardiser les signals, un concept de plus en plus utilisé en développement UI, mérite d'être salué. Même en laissant de côté les débats de détail, comme la quantité de boilerplate ou la nécessité d'écrire soi-même un système d'événements, s'ils sont utilisés dans plusieurs frameworks, c'est peut-être pour une bonne raison, et cela vaut la peine d'essayer de les standardiser, même si cela prend du temps
    • D'accord. Cela dit, si on lit la documentation des signals de Preact, le contexte est bien plus clair
      https://preactjs.com/guide/v10/signals
      Dans Preact, quand un signal descend dans l'arbre via les props ou le context, seule la référence au signal est transmise, et le composant observe le signal plutôt que la valeur, ce qui permet de ne pas re-rendre le composant lors d'une mise à jour du signal. On peut aller directement jusqu'au composant de l'arbre qui accède réellement à .value
      De plus, un signal suit quand sa valeur est accédée et quand elle est mise à jour, et dans Preact, si on accède à .value d'un signal à l'intérieur d'un composant, ce composant se re-rend automatiquement lorsque la valeur du signal change
    • Comme la réactivité n'est pas intégrée à JavaScript, ajouter de la réactivité entraîne inévitablement un coût d'abstraction. C'est quelque chose à utiliser quand on en a besoin, pas forcément une manière par défaut de gérer l'état
      D'après mon expérience, le grand avantage est qu'on peut modulariser l'état réactif. Dans un style impératif, il faut un état supplémentaire pour suivre les changements, et la modularité est obtenue par l'abstraction. Il suffit de l'utiliser quand c'est nécessaire
      Créer des exemples à la fois simples et applicables est une question d'équilibre. Les cas où la réactivité apporte un bénéfice clair sont en général plus complexes, donc plus difficiles à montrer que des exemples simples mais moins représentatifs
    • La manière de l'expliquer peut être améliorée. Le problème se voit mal dans de petits exemples, et apparaît à plus grande échelle. Les PR sont les bienvenues
    • Éviter de devoir changer de conception à partir d'un certain seuil de complexité a de la valeur. L'approche en pur JS a une limite d'évolutivité en termes de complexité du graphe d'état, et le vrai problème n'est pas l'utilisabilité juste avant ou juste après ce seuil, mais le fait qu'une fois ce seuil franchi, l'utilisabilité change de façon discontinue
  • Quand Promises a été ajouté à JavaScript, j'avais une réaction de rejet à l'idée de devoir écrire new Promise partout
    En réalité, je peux compter sur les doigts des deux mains le nombre de fois où j'ai écrit new Promise directement. À la place, j'utilise bien plus souvent .then, surtout quand je travaille avec des bibliothèques tierces
    Au final, l'effet quotidien de l'ajout de Promise à JavaScript a été de fournir une interface assez simple, globalement robuste et presque universelle aux divers comportements spéciaux et fonctionnalités proposés par les bibliothèques tierces. Qu'il s'agisse de lire un fichier, de faire une requête API ou de produire une sortie d'étape de build, écrire .then(res => …) donne déjà l'impression d'avoir fait la moitié du chemin vers quelque chose de fonctionnel
    Si cette proposition de Signal peut jouer un rôle similaire dans l'explosion cambrienne des frameworks UI réactifs, alors j'y suis favorable. Elle pourrait même aider à étendre la réactivité au-delà de l'UI. J'ai souvent imaginé des arbres d'état à recalcul progressif pour des choses qui ne sont pas de l'état d'interface

    • J'ai surtout vu Promises comme un ajout destiné à permettre async/await, et c'est surtout ça qui a réellement amélioré la qualité de vie. En pratique, on a rarement besoin d'écrire new Promise soi-même
      Le .then des débuts était une nette amélioration par rapport aux callbacks imbriqués, et il fonctionne bien pour des chaînes simples, mais dès qu'il faut enchaîner conditionnellement des Promises différentes, gérer les erreurs différemment selon la chaîne, ou faire des retours anticipés, le code peut devenir bien plus difficile à lire et à manipuler
      Avec async/await, on peut écrire les appels comme s'il ne s'agissait pas de Promises, entourer facilement un appel de Promise précis avec un try/catch, et faire des retours anticipés de manière naturelle
  • Je ne vois pas pourquoi cela devrait faire partie du langage. On peut le faire via une bibliothèque, et de telles bibliothèques existent déjà. C’est suffisamment petit pour être inclus dans le code sans grosse contrainte, et l’ajouter au langage ne devrait pas devenir un objectif en soi
    Penser que les bibliothèques UI JS actuelles ont si bien conçu les signals qu’ils doivent faire partie du langage, c’est de l’arrogance. Il existe de nombreuses implémentations de signals avec des compromis différents, et aucune ne mérite une place particulière dans la spécification JavaScript
    Avant d’utiliser les signals, ces bibliothèques utilisaient le DOM virtuel. Heureusement, le DOM virtuel n’est pas devenu une partie de JS, alors en quoi les signals sont-ils différents ? Ils ne le sont pas. L’argument en faveur d’une standardisation est encore plus faible que pour le DOM virtuel
    Va-t-on vraiment empiler dans un runtime, où il n’existe pratiquement aucun moyen de retirer ensuite des fonctionnalités devenues indésirables sans casser le web, tout ce qui est à la mode ? C’est assez court-termiste

    • Il y a du vrai là-dedans. Je ne veux pas de la mauvaise chose, mais je veux la bonne
      Les UI réactives ont gagné. Même dans de petites applications, la gestion d’état fait vite exploser la complexité, et c’est le principal facteur qui rend difficile l’usage du JS pur. Pour moi, n’importe quel framework réactif vaut mieux que du JS pur, et dans ce cas il manque peut-être un élément de base
      Cela fait maintenant une dizaine d’années, donc c’est le moment de réfléchir à une frontière que l’on puisse standardiser. Comme avec Promise, si c’est bien fait, cela peut réduire la complexité de cas d’usage très courants
      Une meilleure évaluation serait de demander : « Est-ce que les frameworks réactifs existants utiliseraient cette proposition ? » Si non, il faut voir pourquoi, ce qui manque, ce qui est superflu, et ce qu’on peut apprendre des UI et de la réactivité dans d’autres langages. Cela vaut la peine de distiller cette expérience dispersée
    • L’une des bonnes raisons de standardiser les signals, c’est que le debugging a l’air d’un cauchemar. Il suffit d’imaginer un arbre profond de signals calculés qui se déclenchent mutuellement en chaîne, et la nécessité de retrouver le point de départ de cette réaction en cascade. Si c’est standardisé, des outils de développement pourront être construits autour
    • On pourrait dire la même chose de la majeure partie de la bibliothèque standard. Mais, comme le dit la motivation, il y a une tendance à étendre la bibliothèque standard relativement petite de JS pour éviter d’avoir à importer un package pour chaque tâche courante
      On peut débattre de la nécessité, mais si l’on va étendre la bibliothèque standard, regarder ce qui est populaire me semble une bonne approche
      les signals ne sont pas un remplacement du DOM virtuel
    • Cela me rappelle la proposition Observable
  • Quand il faut signaler quelque chose à toute l’application, on utilise des événements
    window.dispatchEvent(new Event('counterChange'));
    Et n’importe quelle partie de l’application qui veut réagir peut s’y abonner ainsi
    window.addEventListener('counterChange', () => { ... do something ... });
    Quel est le problème avec cette approche ?

    • Historiquement, cet exemple explique précisément pourquoi le web a évolué vers jQuery, puis s’est séparé entre l’univers Angular et React
      La gestion d’événements devient très facilement désordonnée. Pour aller plus loin, il suffit de regarder le bubbling et la propagation des événements
      Les grandes applications ont besoin d’une gestion d’événements robuste, et c’est l’un des avantages, aujourd’hui moins visibles, de frameworks comme Angular ou Vue
      Vous n’aurez probablement pas envie d’utiliser directement l’API standard de gestion d’événements sans framework. Dès qu’il faut gérer l’ajout, la suppression, la duplication, le déclenchement, le retrait, le déclenchement unique, etc. sur de nombreux éléments, on peut provoquer de graves effets de bord indésirables
    • D’après le texte, les publishers d’événements / observables provoquent du travail inutile lorsqu’ils sont appelés plusieurs fois
      La différence avec les signals, c’est que la valeur résultante n’est calculée que lorsque le consommateur final lit la valeur. Le moment où l’on écrit réellement dans le signal est séparé de la planification asynchrone des mises à jour de rendu, et la chaîne de calcul exécutée par les observateurs n’est effectuée qu’une seule fois pendant le rendu
      Les valeurs intermédiaires envoyées à un signal disparaissent, donc il est difficile d’y faire beaucoup de choses intéressantes ; c’est en pratique plus proche d’une couche d’abstraction avancée destinée à orchestrer le cycle de rendu
    • Les signals restent au fond du publish/subscribe, mais avec une API plus agréable à utiliser. Les listeners sont ajoutés et retirés automatiquement
      Les performances peuvent aussi être meilleures. Par exemple, s’il existe un calcul dépendant de deux valeurs, dans result = a ? b : 0, si a est faux, il n’est pas nécessaire de recalculer quand b change. Avec les signals, cela se fait automatiquement, alors qu’avec un publish/subscribe traditionnel, cela demande pas mal de code
    • J’utilise ce modèle depuis plus de 10 ans. La difficulté, c’est qu’avec le temps un listener peut déclencher un autre événement, puis un autre événement peut revenir à la première routine, ce qui crée une boucle infinie de listeners
      Il est aussi difficile de garantir qu’aucun listener ne produira une telle chaîne de déclenchements
    • Cette approche cumule tous les inconvénients de l’architecture publish/subscribe mis en avant par la proposition
  • Depuis des décennies, j’essaie de comprendre pourquoi les gens trouvent le suivi d’état et les mises à jour du DOM si difficiles
    Bien sûr, un peu de discipline est nécessaire, mais cela me paraît bien plus simple que les solutions qui reviennent tous les quelques années. Backbone, Knockout, Angular, React, les modifications du langage lui-même, etc. J’ai sans doute une façon de penser fondamentalement différente
    Cela se voit même dans le nom des fonctions. On appelle « render » le fait de mettre à jour innerText, alors qu’en réalité rien n’est rendu. Au mieux, c’est le navigateur qui effectue le rendu, et cela vaut tout autant pour toutes les autres opérations liées au painting. J’ai vraiment l’impression qu’on assiste à une tentative désespérée de compliquer une des fonctions DOM les plus simples, et ça me laisse perplexe

    • C’est facile dans une application simple
      Quand cela devient plus complexe, ça ne l’est plus
    • Avec le « un peu de discipline est nécessaire », j’ai vraiment l’impression que, plus jeune, en tant que programmeur ASM, j’aurais râlé contre les programmeurs C fous sur leurs machines portables, puis plus tard, en tant que programmeur C, contre les programmeurs Java fous de sécurité mémoire
      On peut voir le progrès de la programmation comme un processus qui élimine les rituels et la discipline rigide nécessaires pour obtenir de bons résultats
      Cela ne veut pas dire que React est l’évolution suivante, mais les signals sont clairement un pas dans la bonne direction
    • Des décennies, c’est long. Vous devez vous souvenir à quel point les mises à jour du DOM étaient complexes selon les navigateurs
      Synchroniser le DOM avec l’état des données n’est pas si difficile en soi, mais le faire avec d’excellentes performances à 60 fps est extrêmement difficile. Surtout quand il faut concevoir une API qui ne fuit pas, sans pour autant devenir trop pénible à utiliser
      Honnêtement, il peut être plus facile de dessiner des pixels sur un canvas comme dans un jeu que de transformer des changements pour les répercuter sur un arbre DOM vivant
    • Je construis une application de trading FX de plusieurs centaines de milliers de lignes. Elle remplace des applications desktop lourdes, avec 20 à 30 développeurs et plusieurs clients qui ont eux-mêmes leurs propres développeurs. Si vous voulez essayer sans framework, je ne peux que vous souhaiter bonne chance
    • Je pense exactement la même chose. J’ai aussi développé des SPA très complexes et très interactives, et je n’ai toujours jamais rencontré le problème que ces choses prétendent résoudre
  • Les Promises sont un bon exemple de réussite, mais sans async/await, elles n’auraient pas forcément eu besoin d’être standardisées
    L’ébauche actuelle dit s’appuyer sur les conceptions des auteurs/mainteneurs d’Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz, etc., et je me demande comment les auteurs de bibliothèques existantes voient cette proposition. Il est aussi intéressant que React ne figure pas dans la liste
    Les signals ressemblent un peu aux channels, sauf qu’ils diffusent en broadcast au lieu d’avoir un seul récepteur. Ce serait sympa d’exploiter cela pour que les web workers puissent communiquer via des channels plutôt qu’avec des callbacks onMessage. Surtout si, comme en Go, on pouvait faire un select au-dessus de signals/channels/promises, il y aurait alors un avantage syntaxique par rapport à la gestion par callbacks de plusieurs mécanismes de messagerie concurrents. Par exemple, permettre d’inclure des signals dans Promise.any

    • Je suis fortement en désaccord avec l’idée que « sans async/await, il n’aurait pas fallu standardiser »
      x instanceof Promise ne fonctionne tout simplement pas. Si la méthode then de ma bibliothèque accepte un callback catch mais qu’une autre bibliothèque ne l’accepte pas, elles ne seront pas interopérables en silence, et il n’y aura même aucun moyen de le détecter. Quand finally s’exécute-t-il ? Quelles attentes peut-on avoir sur le degré d’asynchronisme avec lequel les callbacks seront exécutés ?
      Sans standard, chaque bibliothèque qui utilise des Promises doit embarquer son propre polyfill. Parce qu’on ne peut pas faire confiance à ce qui existe déjà. Et on ne peut pas non plus réellement consommer les Promises d’autres bibliothèques, parce qu’on ne peut pas croire qu’elles se comporteront comme attendu
      Ce n’était pas une supposition : cela s’est réellement passé pendant des années, et beaucoup de gens ont dû traverser cet enfer
    • Il y a aussi des avantages de la standardisation sans rapport avec async/await. Les moteurs JavaScript ont pu faire des optimisations de performance profitant aux applications qui utilisent beaucoup les Promises, ce qui aurait été impossible sans standard
    • Si React n’apparaît pas dans cette liste, c’est parce que les signals ne font pas partie de l’API centrale de React, contrairement à Preact
      Mon intuition floue est que les signals ressemblent trop à un useEffect() généralisé, et que leur intégration dans React rendrait encore plus confus ce qui se passe pendant le cycle de rendu. En bien ou en mal, React a choisi une approche de mise à jour différente de celle des signals. Mais je peux me tromper sur leur applicabilité
    • Si React n’est pas dans cette liste, c’est parce que ses effets ne sont pas impératifs mais déclaratifs. On peut aussi voir les changements de props et les re-renders comme une forme déclarative à un niveau d’abstraction supérieur. useEffect isole proprement les comportements impératifs
      Cela ressemble beaucoup au data binding d’Ember, et cela peut au final tourner au cauchemar impératif. L’état par défaut se rapproche d’un « fusil braqué sur son propre pied », et il faut une énorme charge cognitive ainsi que des méta-patterns pour éviter que cela n’arrive
    • On pourrait peut-être simplement appeler ça un « EventEmitter »
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • Je n’ai pas compris les exemples du README lié
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    Quelle bibliothèque ? Quel framework ? C’est là que je me suis perdu. Qu’est-ce que effect ?
    effect(() => element.innerText = parity.get());
    Comment effect sait-il qu’il doit appeler cette lambda chaque fois que parity change ? Appelle-t-il cette lambda à chaque changement d’un signal ? Si c’est le cas, pourquoi parler de cache ? Probablement que non
    Quoi qu’il en soit, si j’ai bien compris ce que les auteurs voulaient transmettre, l’idée des signaux en elle-même semble valable. Le gros problème de ce type d’architecture découplée, c’est que quand l’application devient suffisamment complexe, on finit par se perdre en essayant de suivre pourquoi tel événement s’est produit. Idéalement, les signals devraient modifier les traces de pile pour que, quand un callback est appelé, la trace de pile inclue déjà le code qui a déclenché le signal à l’origine

    • Il existe plusieurs bibliothèques qui exportent une fonction appelée effect et permettent d’exécuter du code arbitraire en réaction à une mise à jour de signal. L’introduction aux signals et aux effects dans la documentation de Preact est bien faite : https://preactjs.com/guide/v10/signals#effectfn
      Si j’ai bien compris, ce type de fonction effect exécute d’abord le callback une première fois pour voir à quels signaux il accède pendant son exécution, puis rappelle le callback chaque fois qu’un signal dont il dépend est mis à jour. Si l’accès aux signaux est synchrone et mono-thread, le simple fait d’accéder à un signal pendant l’exécution du callback suffit à savoir que ce callback doit s’abonner à ce signal
      C’est aussi possible avec des getters. La fonction effect peut alors suivre à quelle propriété d’un signal la méthode getter a accédé, et il me semble que Vue 2 utilisait autrefois cette approche. On peut aussi suivre les accès aux objets avec des proxys. L’exemple de la proposition utilise une méthode get appelée pour accéder à la valeur du signal, et l’exécution de cette méthode permet de suivre les dépendances
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • L’appel à parity.get() enregistre une dépendance pour la fonction passée à effect(). Quand parity est mis à jour, cette fonction est appelée
      Elle n’est pas appelée à chaque fois qu’un signal change, seulement quand un signal dont elle dépend change
      Ici, parity dépend de isEven, et isEven dépend de counter. Donc quand counter est mis à jour, toute la chaîne de dépendances est invalidée, parity est invalidé, puis le callback est réexécuté
    • Les implémentations de signals, quel que soit leur nom, construisent en général un graphe de dépendances dynamique, où des arêtes se créent lorsqu’un nœud est lu. Dans un contexte de suivi comme cet effect hypothétique, une lecture crée une arête entre le nœud d’état du signal et le nœud de calcul de l’effect, ce qui revient à faire en sorte que le second s’abonne aux écritures ultérieures du premier, afin de savoir quand relancer le calcul
    • effect est une fonction arbitraire que l’on veut appeler
      Avec les signals, le mécanisme de suivi des dépendances sait quelles valeurs doivent être recalculées, et le système sait donc aussi quelles fonctions doivent être rappelées ensuite
    • Il semble qu’un watcher soit nécessaire pour implémenter effect
  • À ce sujet, il y a S.js : https://github.com/adamhaile/s
    J’aime les signals. Pour construire des UI, je les préfère à n’importe quel autre élément de base, avec peut-être comme seule exception l’algorithme de contraintes cassowary. J’essaie d’imiter les signals dans tous les langages que j’utilise pour le plaisir
    Mais je ne pense absolument pas que cela ait sa place dans le langage JavaScript lui-même. J’aimerais qu’on laisse un peu le langage tranquille pendant un moment. Les gens ont déjà du mal à suivre, et le TC-39 contribue déjà à les intimider et à les éloigner du langage

  • Cela ressemble beaucoup à MobX, qui est mon système d’effets JS préféré
    La version MobX ressemble à ceci
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX est bien un système de signals. Simplement, au lieu de suivre explicitement les dépendances via des getters, il les suit implicitement via des objets proxy
  • Ça donne l’impression de : « Intégrons dans la bibliothèque standard le framework que j’utilise en ce moment ! »
    C’est un peu comme se faire tatouer le nom de sa copine sur le corps

    • Ce n’est pas ça
      Il s’agit d’ajouter à la bibliothèque standard un composant de base vers lequel la plupart des frameworks ont convergé dans leur usage
      Les Promises aussi ont d’abord été largement utilisées avant d’entrer dans la bibliothèque standard, et ici c’est un peu la même chose