Proposition d’ajout de Signals à JavaScript
(github.com/proposal-signals)- 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.ComputedetSignal.subtle.Watcherconstituent 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
Promisedans 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,parityetrenders’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
parityne change pas, par exemple lorsquecounterpasse de 2 à 4 - Lorsqu’un autre fragment d’UI veut ne s’abonner qu’à une partie de
counter,isEvenouparity, 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.StateetSignal.Computedpour 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 deget(): TSignal.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 avecset(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.subtlecontient des API avancées davantage destinées aux auteurs de frameworks ou aux implémentations de DevToolsuntrack(cb)permet de lire un Signal sans suivicurrentComputed()renvoie le Signal computed actuellement suiviintrospectSources,introspectSinks,hasSinksethasSourcessont des API d’observation du grapheWatchersert 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éeequalsainsi que les hookswatched/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
stateoucomputed - 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é
- Après
- Le callback
notifyd’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
- Toutefois, il n’est pas possible de lire ou d’écrire un Signal pendant
- 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,
untracket 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
notifyd’un Watcher untrackest 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.Watcherfournit une base de bas niveau pour implémenter des effects- Lorsque les dépendances d’un Signal observé changent,
notifyest appelé getPending()permet de vérifier quels Signals sont encore dirty- Les effects nécessitant une disposal doivent être nettoyés avec
unwatch
- Lorsque les dépendances d’un Signal observé changent,
- Les Signals surveillés par un Watcher peuvent rester vivants tant que leur state interne est accessible ; il faut donc appeler
Watcher.prototype.unwatchlors 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
- Le framework gère les Watchers,
- 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
Proxysont 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
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
counterpasse de 2 à 4,parityne change pas mais on fait quand même des calculs et des rendus inutiles » ressemble à une mémoïsation prématuréeSi 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
isEvenou deparity, 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 deparitydoive 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 à identifierTenter 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
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 à
.valueDe plus, un signal suit quand sa valeur est accédée et quand elle est mise à jour, et dans Preact, si on accède à
.valued'un signal à l'intérieur d'un composant, ce composant se re-rend automatiquement lorsque la valeur du signal changeD'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
Quand Promises a été ajouté à JavaScript, j'avais une réaction de rejet à l'idée de devoir écrire
new PromisepartoutEn réalité, je peux compter sur les doigts des deux mains le nombre de fois où j'ai écrit
new Promisedirectement. À la place, j'utilise bien plus souvent.then, surtout quand je travaille avec des bibliothèques tiercesAu 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 fonctionnelSi 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
new Promisesoi-mêmeLe
.thendes 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 à manipulerAvec 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 naturelleJe 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
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
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
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 ?
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
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 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 codeIl est aussi difficile de garantir qu’aucun listener ne produira une telle chaîne de déclenchements
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 perplexeQuand cela devient plus complexe, ça ne l’est plus
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
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
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 unselectau-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 dansPromise.anyx instanceof Promisene fonctionne tout simplement pas. Si la méthodethende 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. Quandfinallys’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
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éuseEffectisole proprement les comportements impératifsCela 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
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 primitivesdeclare 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
effectsait-il qu’il doit appeler cette lambda chaque fois queparitychange ? Appelle-t-il cette lambda à chaque changement d’un signal ? Si c’est le cas, pourquoi parler de cache ? Probablement que nonQuoi 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
effectet 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#effectfnSi 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
getappelé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...
parity.get()enregistre une dépendance pour la fonction passée àeffect(). Quandparityest mis à jour, cette fonction est appeléeElle n’est pas appelée à chaque fois qu’un signal change, seulement quand un signal dont elle dépend change
Ici,
paritydépend deisEven, etisEvendépend decounter. Donc quandcounterest mis à jour, toute la chaîne de dépendances est invalidée,parityest invalidé, puis le callback est réexécutéeffecthypothé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 calculeffectest une fonction arbitraire que l’on veut appelerAvec 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
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);Ç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
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