3 puntos por GN⁺ 2024-04-01 | 1 comentarios | Compartir por WhatsApp
  • La propuesta de JavaScript Signals de TC39 es una dirección inicial para estandarizar primitivas reactivas que permitan rastrear de forma eficiente el estado de la UI y el estado calculado, y actualmente es un borrador en nivel Stage 1
  • La propuesta se enfoca más en la semántica central del grafo de Signals y en los mecanismos de rastreo automático que los frameworks pueden compartir, que en una API superficial para uso directo de desarrolladores de aplicaciones
  • Signal.State, Signal.Computed y Signal.subtle.Watcher son las API clave, y los cálculos apuntan a lazy evaluation, caché, rastreo automático de dependencias y ejecución libre de glitches
  • El objetivo de los Signals integrados es aumentar la interoperabilidad entre varios frameworks como Angular, Ember, MobX, Preact, Qwik, Solid, Svelte y Vue, además de abrir posibilidades de soporte para depuración y análisis de rendimiento en DevTools
  • El grupo de la propuesta planea pasar por varios polyfills de nivel producción, integraciones con frameworks, validación en aplicaciones grandes y benchmarks de rendimiento antes de Stage 2, y la estandarización podría tardar al menos 2 a 3 años o más

La posición actual de la propuesta JavaScript Signals

  • JavaScript Signals se presenta como una propuesta Stage 1 dentro del proceso de TC39
  • El documento actual es un intento de alinear una dirección común para el ecosistema de JavaScript, parecido a lo que fueron Promises/A+ antes de la estandarización de Promise en ES2015
  • Hay un polyfill disponible para probarlo directamente
  • Los champions de la propuesta y autores originales armaron el borrador actual con base en aportes de diseño de varios frameworks y librerías

El problema al que apunta la estandarización

  • Las interfaces complejas tienen que almacenar valores, calcularlos, invalidarlos, sincronizarlos y empujarlos hacia la capa de vista, y Signals busca ofrecer una infraestructura de manejo de estado para ese trabajo repetitivo
  • En un ejemplo de Vanilla JS, counter, isEven, parity y render quedan directamente entrelazados y aparecen los siguientes problemas
    • El estado y el sistema de renderizado quedan fuertemente acoplados
    • Aunque parity no cambie, como cuando counter pasa de 2 a 4, ocurren cálculos y renderizados innecesarios
    • Si otras partes de la UI quieren suscribirse solo a counter, isEven o parity, la gestión manual de suscripciones y cancelaciones se vuelve compleja
    • Si se agrega pub/sub en varias capas, aumentan el boilerplate y el bookkeeping de suscripciones, además del riesgo de fugas de memoria
  • Un ejemplo basado en Signals usa Signal.State y Signal.Computed para tratar valores, cálculos y efectos secundarios de una sola manera
    • No se necesitan suscripciones manuales
    • Los Signals calculados detectan automáticamente de qué Signals dependen
    • Los cálculos solo se ejecutan cuando alguien pide explícitamente el valor
    • Los Signals calculados guardan en caché el último valor

API principal de la propuesta

  • Signal<T> se define como un valor legible que tiene get(): T
  • Signal.State<T> es un Signal escribible
    • El constructor recibe un valor inicial y opciones
    • Se lee con get() y se modifica con set(t)
  • Signal.Computed<T> es un Signal calculado basado en otros Signals
    • Se calcula a partir del valor que devuelve un callback
    • Rastrea dependencias automáticamente
    • Su valor se calcula de forma lazy y queda en caché
  • Signal.subtle contiene API avanzadas, más cercanas a autores de frameworks o implementaciones de DevTools
    • untrack(cb) permite leer un Signal sin rastreo
    • currentComputed() devuelve el computed Signal que se está rastreando en ese momento
    • introspectSources, introspectSinks, hasSinks y hasSources son API para observar el grafo
    • Watcher es la base para implementar efectos a nivel framework y scheduling al detectar cambios en Signals
  • SignalOptions<T> soporta una función de comparación personalizada equals y hooks watched / unwatched

Funcionamiento y modelo de ejecución

  • Un Signal representa una celda de datos que puede cambiar con el tiempo, y se divide en state o computed
  • Un Signal calculado registra automáticamente los Signals que leyó durante su ejecución, y luego cuando vuelve a leerse verifica si cambiaron sus dependencias anteriores
  • Los cálculos son pull-based
    • Aunque cambien las dependencias, no se recalculan de inmediato
    • Se recalculan cuando hace falta y alguien los lee con .get()
  • Las escrituras en State Signals se reflejan de manera síncrona
    • Después de .set(), si se lee un computed Signal que depende de ese valor, se recalcula de inmediato si hace falta
    • No hay batching integrado
  • El callback notify de un Watcher puede ejecutarse de forma síncrona durante .set()
    • Pero durante notify no se puede leer ni escribir Signals
    • El trabajo real de lectura o escritura debe programarse después
  • Si el callback de un computed Signal lanza una excepción, esa excepción también se guarda en caché como si fuera un valor, y se vuelve a lanzar al releer el Signal

Motivos para la estandarización

  • Las implementaciones de Signals de cada framework tienen su propio mecanismo de rastreo automático, lo que dificulta compartir modelos, componentes y librerías entre frameworks
  • El objetivo de la propuesta es separar el modelo reactivo de la vista de renderizado
    • Para que los desarrolladores no tengan que reescribir el código no relacionado con UI al cambiar de tecnología de renderizado
    • Para permitir crear en JavaScript modelos reactivos que puedan compartirse entre varios contextos
  • En rendimiento y memoria, se indica que una implementación integrada podría ser más eficiente que una en JS por un factor constante menor, pero que el motor no cambiará mágicamente el algoritmo
  • Desde el lado de DevTools, Signals integrados podrían mostrar mejor la siguiente información
    • El call stack de cadenas de computed Signals
    • El grafo de referencias entre Signals
    • Las relaciones de dependencia necesarias para depurar el uso de memoria
  • Si llegan a formar parte de la librería estándar, también se esperan efectos secundarios como reducción del tamaño del bundle, mejor estabilidad y calidad, y una terminología común entre proyectos

Objetivos y restricciones de diseño

  • Las funciones centrales incluyen Signals escribibles, Signals calculados, reacción al estado dirty, scheduling propio del framework, untrack y composición entre varias bases de código
  • Los computed Signals apuntan a ser glitch-free
    • Para evitar cálculos innecesarios, ejecutan las partes potencialmente dirty del grafo en orden topológico
    • Buscan eliminar cálculos duplicados
  • No se incluye un scheduling forzado integrado al estilo Promise, para que los frameworks puedan manejar su propio scheduling
  • Para evitar el mal uso de callbacks de reacción síncrona, dentro de notify de Watcher está prohibido leer y escribir Signals
  • untrack se considera una vía de escape insegura
    • Si un Signal leído sin rastreo influye en el resultado del cálculo, el computed Signal podría no actualizarse cuando ese Signal cambie
  • La API prioriza servir como base para implementaciones de frameworks, y no está diseñada especialmente para ser ergonómica para desarrolladores de aplicaciones en general

effect y Watcher

  • La propuesta no incluye una función integrada como effect()
  • El scheduling de effects está entrelazado con el ciclo de render del framework, disposal y manejo de ownership, así que la API estándar de JavaScript no intenta resolverlo directamente
  • En cambio, Signal.subtle.Watcher ofrece una base de bajo nivel para implementar effects
    • notify se llama cuando cambian las dependencias de los Signals observados
    • getPending() permite ver qué Signals siguen dirty
    • Los effects que necesitan cleanup deben limpiarse con unwatch
  • Los Signals que un Watcher está observando pueden seguir vivos mientras su estado interno siga siendo alcanzable, por lo que al limpiar un effect hace falta llamar a Watcher.prototype.unwatch

Funciones que faltan en el borrador actual

  • Async no está incluido en el modelo actual
    • Signals siempre se tratan como valores evaluables de forma síncrona
    • Hay cierta posibilidad de modelar el estado de loading como excepción, y la discusión de mejoras está en Issue #30
  • Transactions tampoco está incluido
    • Para mantener al mismo tiempo un estado “from” y un estado “to” en una transición de pantalla, aparece el problema de hacer fork del estado del grafo de Signals
    • La discusión relacionada está en Issue #73
  • Algunos convenience methods también están fuera del borrador actual
  • Estas funciones quedaron fuera por falta de consenso entre frameworks y porque pueden resolverse en capas superiores, aunque podrían revisarse de nuevo después del prototipo

Plan de desarrollo y calendario de estandarización

  • Esta propuesta estuvo en la agenda de TC39 Stage 1 en abril de 2024, y el documento explica que por ahora también puede verse como si estuviera en Stage 0
  • Antes de proponer Stage 2, planean el siguiente trabajo
    • Desarrollar varios polyfills de nivel producción
    • Probar en distintos frameworks y pasar tests al estilo test262
    • Verificar rendimiento con un conjunto exhaustivo de benchmarks de signals/frameworks
    • Integrar la API propuesta en varios frameworks representativos de JS y en algunas aplicaciones grandes
    • Entender la capacidad de extensión de la API y decidir qué incluir
  • El grupo de la propuesta quiere avanzar con cautela para evitar que una forma incorrecta de Signals se estandarice demasiado pronto
  • El FAQ estima que pasarán al menos 2 a 3 años antes de que los Signals estándar puedan usarse en todos los navegadores sin polyfill
  • El polyfill actual puede usarse, pero se advierte que la API podría cambiar durante la revisión, por lo que conviene no depender de su estabilidad

Modelo de uso resumido en el FAQ

  • Los Signals integrados son independientes de la tecnología de renderizado
    • El documento explica que son posibles enfoques como Preact con VDOM, Solid con DOM nativo o Vue con enfoque mixto
  • Para desarrolladores de aplicaciones, normalmente tiene más sentido usar Signals a través de un framework
    • El framework administra Watcher, untrack, ownership, disposal y el scheduling del render de DOM
  • Puede usarse junto con SSR, hydration y resumability
    • Qwik ya usa Signals con estas propiedades, y quienes impulsan la propuesta consideran que los Signals reanudables de Qwik pueden modelarse como una combinación de State y Computed
  • Signals y Proxy son complementarios
    • Proxy intercepta operaciones sobre objetos superficiales, y Signals coordina el grafo de dependencias de las celdas de datos
    • Poner Signals detrás de Proxy puede hacer más ergonómicas las estructuras reactivas anidadas
  • Signals no son streams, sino celdas que representan el valor actual
    • Si se escribe dos veces seguidas en un State Signal y no pasa nada más, la primera escritura podría no ser visible para un computed Signal o un effect
    • El documento considera esto como una característica opuesta al comportamiento de los streams, y dice que para streams son más adecuadas otras construcciones como async iterable u observable

1 comentarios

 
GN⁺ 2024-04-01
Opiniones de Hacker News
  • ¿Soy el único al que el ejemplo en JavaScript puro le parece más fácil de leer y manejar?
    Dicen que “la configuración tiene mucho ruido y boilerplate”, pero el ejemplo con signals se ve igual de ruidoso y lleno de boilerplate, y además agrega un concepto nuevo difícil de entender para principiantes.
    Eso de que “si counter cambia de 2 a 4, parity no cambia, pero se hacen cálculos y renderizados innecesarios” suena a memoización prematura.
    Si otra parte de la UI quiere renderizarse cuando se actualiza counter, es cierto que ese ejemplo strawman no es adecuado; en ese caso se pueden usar otros enfoques como signals, manejo de eventos o un almacén central de estado (tipo Redux).
    Si otra parte de la UI depende solo de isEven o parity, y eso es parte de la estructura central de la app, quizá se podría cambiar el enfoque, pero en la mayoría de los casos no es así. Que “una función render que solo depende de parity tenga que saber que debe suscribirse a counter” tampoco es necesariamente una carga injusta, y las funciones de cálculo puras tienen la ventaja de que es fácil identificar sus entradas.

    • No entiendo por qué se ve esto como memoización prematura. Es solo un ejemplo reducido a una función simple, y cuesta creer que la gente haya inventado este caso de uso sin haberlo necesitado realmente.
      Intentar estandarizar signals, un concepto cada vez más usado en desarrollo de UI, es algo digno de elogio. Dejando de lado discusiones de detalle como cuánto boilerplate hay o si hay que construir un sistema de eventos propio, si varios frameworks usan signals puede haber una razón, y vale la pena intentar estandarizarlos aunque tome tiempo.
    • De acuerdo. Dicho eso, la documentación de signals de Preact da mucho más contexto.
      https://preactjs.com/guide/v10/signals
      En Preact, cuando un signal baja por el árbol como props o context, solo se pasa la referencia al signal, y como los componentes observan el signal y no el valor, actualizar el signal puede no volver a renderizar el componente. En la práctica, se puede ir directamente al componente del árbol que accede a .value.
      Además, un signal rastrea cuándo se accede a su valor y cuándo se actualiza; en Preact, si dentro de un componente se accede a .value de un signal, el componente se vuelve a renderizar automáticamente cuando cambia el valor de ese signal.
    • Como JavaScript no trae reactividad incorporada, agregar reactividad necesariamente introduce un costo de abstracción. Es algo para usar cuando hace falta, no tiene por qué ser la forma predeterminada de manejar estado.
      Por experiencia, una gran ventaja es que permite modularizar el estado reactivo. En un estilo imperativo se necesita estado adicional para rastrear cambios, y la modularidad se logra mediante abstracciones. Hay que usarlo solo cuando hace falta.
      Crear un ejemplo simple pero aplicable es una cuestión de equilibrio. Los casos donde la reactividad es claramente beneficiosa suelen ser más complejos, así que son más difíciles de mostrar que un ejemplo simple pero menos aplicable.
    • La forma de explicar esto puede mejorarse. En ejemplos pequeños el problema no se ve bien; aparece a mayor escala. Se aceptan PR.
    • Vale la pena evitar que, al superar cierto umbral de complejidad, haga falta un cambio de diseño. El enfoque de JS puro tiene un límite de escalabilidad en términos de complejidad del grafo de estado, y el verdadero problema no es la usabilidad antes o después del umbral, sino que la usabilidad cambia de forma discontinua en el momento en que se cruza ese umbral.
  • Cuando se agregaron Promises a JavaScript, me generaba rechazo pensar que habría que usar new Promise por todas partes.
    En la práctica, puedo contar con los dedos de las dos manos las veces que escribí new Promise directamente. En cambio, terminé usando .then mucho más seguido, especialmente al trabajar con librerías de terceros.
    Al final, el efecto cotidiano de que Promise se agregara a JavaScript fue ofrecer una interfaz bastante simple, en general robusta y casi universal para las distintas conductas y funcionalidades especiales que ofrecen las librerías de terceros. Ya sea leer un archivo, hacer una solicitud a una API o producir una salida en una etapa de build, al usar .then(res => …) uno siente que ya está a medio camino de algo que funciona.
    Si esta propuesta de Signal cumple un rol parecido en medio de la explosión cámbrica de frameworks de UI reactivos, estoy a favor. Incluso podría ayudar a que la reactividad se extienda fuera de la UI. A menudo he imaginado árboles de estado de recálculo incremental para cosas que no son estado de UI.

    • Yo veía que Promises entraron principalmente para agregar async/await, y eso fue lo que realmente mejoró mucho la calidad de vida. En el trabajo práctico rara vez hace falta escribir new Promise directamente.
      El .then de las Promises iniciales era una gran mejora frente a callbacks anidados y está bien para cadenas simples, pero cuando hay que encadenar distintas Promises de forma condicional, manejar errores de manera diferente en cada cadena o hacer retornos tempranos, el código puede volverse mucho más difícil de leer y manejar.
      Con async/await se pueden escribir las llamadas como si no fueran Promises, poner fácilmente try/catch alrededor de una llamada específica a Promise y hacer retornos tempranos de forma natural.
  • No entiendo por qué esto debería convertirse en parte del lenguaje. Se puede hacer con bibliotecas, y ya existen bibliotecas así. Como es pequeño, incluirlo en el código no supone una gran carga, y agregar cosas al lenguaje no debería ser un objetivo en sí mismo.
    Es arrogante pensar que, como las bibliotecas de UI actuales de JS diseñaron tan bien los signals, deberían formar parte del lenguaje. Hay muchas implementaciones de signals con distintos compromisos, y ninguna de ellas merece un lugar especial en la especificación de JavaScript.
    Antes de usar signals, estas bibliotecas usaban DOM virtual. Por suerte, el DOM virtual no pasó a formar parte de JS; ¿qué tienen de distinto los signals? Nada. El argumento para estandarizarlos es incluso más débil que en el caso del DOM virtual.
    ¿Vamos a apilar todo lo que esté de moda en un runtime donde prácticamente no hay forma de quitar funciones que ya no queremos sin romper la web? Es bastante cortoplacista.

    • Hay algo de razón ahí. No quiero algo incorrecto, pero sí quiero lo correcto.
      La UI reactiva ganó. Incluso en aplicaciones pequeñas, al manejar estado la complejidad se dispara, y eso es lo principal que hace difícil usar JS puro. Para mí, cualquier framework reactivo es mejor que JS puro, y si eso es así, quizá falte algún componente.
      Ya pasaron unos 10 años, así que es momento de pensar en qué límite podría estandarizarse. Como con Promise, si se hace bien, podría reducir la complejidad de casos de uso muy comunes.
      Una mejor evaluación sería preguntar: “¿los frameworks reactivos existentes usarían esta propuesta?”. Si no, habría que ver por qué, qué falta, qué sobra y qué podemos aprender de la UI y la reactividad en otros lenguajes. Vale la pena depurar esa experiencia dispersa.
    • Una buena razón para estandarizar Signals es que la depuración parece una pesadilla. Imagina un árbol profundo de signals calculados que se disparan unos a otros en cadena, y tener que encontrar el punto de inicio de esa reacción en cadena. Si se estandariza, las herramientas de desarrollo podrían construirse alrededor de eso.
    • Se podría decir lo mismo de la mayor parte de la biblioteca estándar. Pero, como se menciona en la motivación, hay una tendencia a ampliar la biblioteca estándar relativamente pequeña de JS para no tener que traer un paquete por cada tarea común.
      Esa necesidad se puede debatir, pero si se va a ampliar la biblioteca estándar, mirar lo popular me parece un buen enfoque.
      Los signals no son un reemplazo del DOM virtual.
    • Me recuerda a la propuesta de Observable.
  • Cuando necesitas señalar algo a toda la aplicación, usas eventos.
    window.dispatchEvent(new Event('counterChange'));
    Y cualquier parte de la aplicación que quiera reaccionar puede suscribirse así:
    window.addEventListener('counterChange', () => { ... do something ... });
    ¿Qué tiene de malo este enfoque?

    • Históricamente, este ejemplo es justamente la razón por la que la web evolucionó hacia jQuery y, desde ahí, se dividió en los mundos de Angular y React.
      El manejo de eventos se vuelve desordenado con mucha facilidad. Para verlo más a fondo, mira el bubbling y la propagación de eventos.
      Las aplicaciones grandes necesitan un manejo de eventos robusto, y esa es una ventaja de frameworks como Angular y Vue que hoy no siempre se nota.
      No vas a querer usar tal cual la API estándar de manejo de eventos sin un framework. Cuando tienes que manejar altas, bajas, duplicación, disparos, eliminación, disparos de una sola vez, etc., para muchos elementos, pueden aparecer efectos secundarios no deseados bastante graves.
    • Según el texto, los publicadores de eventos/observables provocan trabajo innecesario cuando se llaman varias veces.
      La diferencia con signals es que el valor resultante solo se calcula cuando el consumidor final lee el valor. Se separa el momento en que se escribe realmente en el signal de la programación asíncrona de la actualización de render, y la cadena de cálculos que realiza el observador se ejecuta solo una vez durante el render.
      Los valores intermedios enviados mediante el signal desaparecen, así que es difícil hacer muchas cosas interesantes dentro de ellos; en la práctica, se parecen más a una capa de abstracción avanzada para coordinar el ciclo de renderizado.
    • Signals también son, al final, publicación/suscripción, pero con una API más cómoda de usar, porque los listeners se agregan y se eliminan automáticamente.
      También pueden tener mejor rendimiento. Por ejemplo, si hay un cálculo que depende de dos valores, result = a ? b : 0, cuando a es falso no hace falta recalcular aunque b cambie. Con signals esto ocurre automáticamente, pero con publicación/suscripción tradicional hace falta bastante código.
    • He usado este patrón durante más de 10 años. Lo difícil es que, con el tiempo, un listener puede disparar otro evento, y luego otro evento puede volver a la primera rutina, creando un bucle interminable de listeners.
      También es difícil garantizar que todos los listeners no creen ese tipo de disparos en cadena.
    • Ese enfoque tiene todas las desventajas de la arquitectura de publicación/suscripción que destaca la propuesta.
  • Durante décadas he intentado entender por qué a la gente le resulta tan difícil el seguimiento del estado y las actualizaciones del DOM.
    Claro que hace falta un poco de disciplina, pero me parece mucho más simple que las soluciones que aparecen cada pocos años. Backbone, Knockout, Angular, React, modificar el propio lenguaje, etc.; quizá mi forma de pensar sea fundamentalmente distinta.
    Incluso se nota en el nombre de las funciones. A actualizar innerText le llaman “render”, pero en realidad no estás renderizando. En el mejor de los casos, el navegador es el que renderiza, y lo mismo aplica a todo lo demás relacionado con el pintado. Me resulta realmente desconcertante, como un intento desesperado de complicar una de las funciones más simples del DOM.

    • En aplicaciones simples es fácil.
      Cuando se vuelven más complejas, ya no lo es.
    • Con eso de “hace falta un poco de disciplina”, me da la fuerte impresión de que antes, como programador de ASM, te habrías enojado con esos locos programadores de C portable, y después, como programador de C, con esos locos programadores de Java con seguridad de memoria.
      El progreso en la programación puede verse como el proceso de eliminar los rituales y la disciplina estricta necesarios para obtener buenos resultados.
      No digo que React sea la siguiente evolución, pero signals claramente son un paso en la dirección correcta.
    • Varias décadas es mucho tiempo. Seguro recuerdas lo complejo que era actualizar el DOM en distintos navegadores.
      Sincronizar el DOM con el estado de los datos no es demasiado difícil en sí, pero hacerlo con muy buen rendimiento a 60fps es tremendamente difícil. Sobre todo cuando también quieres crear una API que no tenga fugas y que no sea demasiado engorrosa.
      Sinceramente, puede ser más fácil dibujar píxeles en un canvas como en un juego que transformar cambios y reflejarlos en un árbol DOM vivo.
    • Estamos construyendo una aplicación de trading FX de cientos de miles de líneas. Es de un nivel que reemplaza a una app de escritorio pesada, con 20 a 30 desarrolladores y varios clientes que tienen sus propios desarrolladores. Si quieres intentarlo sin framework, solo puedo desearte suerte.
    • Pienso exactamente lo mismo. También he desarrollado SPA muy complejas y con mucha interacción, pero todavía no me he topado con el problema que supuestamente resuelven estas cosas.
  • Promises son un buen caso de éxito, pero si no hubiera existido async/await, no habría sido estrictamente necesario estandarizarlas.
    El borrador actual dice estar basado en diseños de autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz, etc., y me da curiosidad saber cómo ven esta propuesta los autores de las bibliotecas existentes. También es interesante que React no esté en la lista.
    Signals se parecen un poco a los canales, pero se diferencian en que son de difusión en vez de tener un único receptor. Sería genial si se pudiera aprovechar esto para que los web workers se comuniquen mediante canales en lugar de callbacks onMessage. Especialmente si, como en Go, se pudiera hacer select sobre signals/channels/promises; eso tendría ventajas sintácticas frente a manejar varios mecanismos de mensajería concurrente con callbacks. Por ejemplo, permitir incluir signals en Promise.any.

    • Estoy totalmente en desacuerdo con que “si no hubiera existido async/await, no habría sido necesario estandarizarlas”.
      x instanceof Promise simplemente no funciona. Si el método then de mi biblioteca recibe un callback de catch y el de otra biblioteca no, silenciosamente no interoperan y no hay forma de detectarlo. ¿Cuándo se ejecuta finally? ¿Qué expectativas puedes tener sobre qué tan asincrónicamente se ejecutarán los callbacks?
      Sin un estándar, toda biblioteca que use Promise tendría que traer su propio polyfill, porque no puede confiar en lo que ya existe. Y tampoco puede consumir realmente Promises de otras bibliotecas, porque no puede confiar en que se comporten de la forma esperada.
      No es una conjetura: así fue en la práctica durante años, y fue un infierno que mucha gente tuvo que soportar.
    • También hay beneficios de la estandarización que no tienen que ver con async/await. Los motores de JavaScript pudieron hacer optimizaciones de rendimiento que benefician a las aplicaciones que usan mucho Promise, y eso no habría sido posible si no fuera un estándar.
    • La razón por la que React no aparece en la lista es que signals, a diferencia de Preact, no forman parte de la API central de React.
      Mi intuición vaga es que signals se parecen demasiado a un useEffect() generalizado, y que si entraran en React harían más confuso qué ocurre durante el ciclo de renderizado. Para bien o para mal, React eligió un enfoque de actualización distinto al de signals. Aunque podría estar equivocado sobre su aplicabilidad.
    • La razón por la que React no está en esta lista es que su efecto es declarativo, no imperativo. Los cambios de props y el rerenderizado también pueden verse como algo declarativo con un nivel de abstracción adicional. useEffect aísla limpiamente el comportamiento imperativo.
      Esto se parece mucho al data binding de Ember, y al final puede convertirse en una pesadilla imperativa. El estado por defecto se parece más a “un arma apuntándote al pie”, y evitar que termine así requiere una enorme carga cognitiva y metapatrones.
    • Quizá se le podría llamar “EventEmitter”.
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • No entendí el ejemplo del README enlazado
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    ¿Qué biblioteca? ¿Qué framework? Ahí me perdí. ¿Qué es effect?
    effect(() => element.innerText = parity.get());
    ¿Cómo sabe effect que debe llamar a esta lambda cada vez que cambia parity? ¿Llama a esta lambda cada vez que cambia un signal? Si es así, ¿por qué se habla de caché? Probablemente no sea eso
    En todo caso, si entendí bien lo que los autores querían transmitir, la idea de los signals en sí parece válida. Pero el gran problema de esta arquitectura desacoplada es que, cuando la aplicación se vuelve lo suficientemente compleja, uno se pierde tratando de rastrear por qué ocurre cierto evento. Idealmente, los signals deberían modificar el stack trace para que, cuando se llame un callback, ya incluya el stack trace del código que disparó originalmente el signal

    • Hay varias bibliotecas que exponen una función llamada effect, que permite ejecutar código arbitrario en reacción a actualizaciones de signals. La introducción a signals y effects en la documentación de Preact es buena: https://preactjs.com/guide/v10/signals#effectfn
      Según entiendo, este tipo de función effect primero ejecuta el callback una vez para ver a qué signals se accedió durante la ejecución, y vuelve a llamar el callback cada vez que se actualiza un signal del que depende. Si el acceso al signal es sincrónico y de un solo hilo, el simple hecho de acceder a un signal durante la ejecución del callback permite saber que ese callback debe suscribirse a ese signal
      También se puede hacer con getters. La función effect rastrea a qué propiedad de un signal se accedió desde el método getter, y tengo entendido que Vue 2 usaba este enfoque antes. También se puede rastrear el acceso a objetos con proxies. El ejemplo de la propuesta tiene un método get que se llama para acceder al valor del signal, y mediante la ejecución de ese método se pueden rastrear las dependencias
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • La llamada a parity.get() registra una dependencia para la función pasada a effect(). Cuando se actualiza parity, se llama a esa función
      No se llama cada vez que cambia un signal, sino solo cuando cambia un signal del que depende
      En este caso, parity depende de isEven, e isEven depende de counter. Así que, cuando counter se actualiza, se invalida toda la cadena de dependencias y, al invalidarse parity, el callback se ejecuta de nuevo
    • Las implementaciones de signals, se llamen como se llamen, por lo general construyen un grafo dinámico de dependencias, y al leer un nodo se crea una arista. En un contexto de rastreo como este effect hipotético, una lectura crea una arista entre el nodo de estado del signal y el nodo de cálculo del effect, haciendo en la práctica que este último se suscriba a escrituras posteriores del primero para decidir cuándo volver a ejecutar el cálculo
    • effect es una función arbitraria que uno quiere llamar
      En signals, el mecanismo de seguimiento de dependencias sabe qué valores deben recalcularse y, como resultado, el sistema también sabe qué funciones debe volver a llamar
    • Parece que se necesitaría un watcher para implementar effect
  • Relacionado con esto, existe S.js: https://github.com/adamhaile/s
    Me gustan los signals. Al crear UI los prefiero por encima de casi cualquier otro primitivo, quizá con la única excepción del algoritmo de restricciones cassowary. Intento imitar signals en todos los lenguajes que uso por diversión
    Pero no creo en absoluto que sea algo que deba entrar en el lenguaje JavaScript en sí. Ojalá dejaran tranquilo al lenguaje por un tiempo. A la gente ya le cuesta seguirle el ritmo, y TC-39 ya está haciendo que la gente se asuste del lenguaje y se aleje

  • Esto se parece mucho a MobX, mi sistema de effects favorito en JS
    La versión de MobX sería así
    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 sí son signals. Solo que, en vez de rastrear las dependencias explícitamente con getters, son signals que las rastrean implícitamente mediante objetos proxy
  • Se siente como “¡metamos en la biblioteca estándar el framework que uso últimamente!”
    Es parecido a tatuarse el nombre de tu novia en el cuerpo

    • No es eso
      Es poner en la biblioteca estándar un componente básico hacia el que la mayoría de los frameworks han convergido para usar
      Las Promises también entraron a la biblioteca estándar después de usarse ampliamente, y esto es parecido