Etapa 1 (explicación)
Campeones de la propuesta TC39: Daniel Ehrenberg, Yehuda Katz, Jatin Ramanathan, Shay Lewis, Kristen Hewell Garrett, Dominic Gannaway, Preston Sego, Milo M, Rob Eisenberg
Autores originales: Rob Eisenberg y Daniel Ehrenberg
Este documento describe una dirección común temprana para señales en JavaScript, similar al esfuerzo Promises/A+ que precedió a las Promises estandarizadas por TC39 en ES2015. Pruébelo usted mismo, utilizando un polyfill.
De manera similar a Promises/A+, este esfuerzo se centra en alinear el ecosistema de JavaScript. Si esta alineación tiene éxito, entonces podría surgir un estándar basado en esa experiencia. Varios autores del marco están colaborando aquí en un modelo común que podría respaldar su núcleo de reactividad. El borrador actual se basa en aportaciones de diseño de los autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz y más...
A diferencia de Promises/A+, no estamos tratando de resolver una API de superficie común orientada al desarrollador, sino más bien la semántica central precisa del gráfico de señal subyacente. Esta propuesta incluye una API totalmente concreta, pero la API no está dirigida a la mayoría de los desarrolladores de aplicaciones. En cambio, la API de señales aquí se adapta mejor a los marcos sobre los que construir, proporcionando interoperabilidad a través de un gráfico de señales común y un mecanismo de seguimiento automático.
El plan para esta propuesta es crear prototipos tempranos significativos, incluida la integración en varios marcos, antes de avanzar más allá de la Etapa 1. Solo estamos interesados en estandarizar las señales si son adecuadas para su uso en la práctica en múltiples marcos y brindan beneficios reales sobre los marcos. señales proporcionadas. Esperamos que los primeros prototipos importantes nos brinden esta información. Consulte "Estado y plan de desarrollo" a continuación para obtener más detalles.
Para desarrollar una interfaz de usuario (UI) complicada, los desarrolladores de aplicaciones JavaScript necesitan almacenar, calcular, invalidar, sincronizar y enviar el estado a la capa de vista de la aplicación de manera eficiente. Las UI comúnmente implican algo más que solo administrar valores simples, sino que a menudo implican representar un estado calculado que depende de un árbol complejo de otros valores o un estado que también se calcula. El objetivo de Signals es proporcionar infraestructura para gestionar el estado de dicha aplicación, de modo que los desarrolladores puedan centrarse en la lógica empresarial en lugar de en estos detalles repetitivos.
Se ha descubierto de forma independiente que las construcciones similares a señales también son útiles en contextos que no son de interfaz de usuario, particularmente en sistemas de compilación para evitar reconstrucciones innecesarias.
Las señales se utilizan en la programación reactiva para eliminar la necesidad de gestionar las actualizaciones en las aplicaciones.
Un modelo de programación declarativa para actualización basada en cambios de estado.
de ¿Qué es la reactividad? .
Dada una variable, counter
, desea representar en el DOM si el contador es par o impar. Cada vez que cambia el counter
, desea actualizar el DOM con la última paridad. En Vanilla JS, es posible que tengas algo como esto:
let contador = 0;const setCounter = (valor) => { contador = valor; render();};const esEven = () => (contador & 1) == 0;const paridad = () => esEven() ? "par" : "impar";const render = () => element.innerText = parity();// Simular actualizaciones externas para contador...setInterval(() => setCounter(counter + 1), 1000);
Esto tiene varios problemas...
La configuración counter
es ruidosa y llena de textos repetitivos.
El estado counter
está estrechamente acoplado al sistema de renderizado.
Si el counter
cambia pero parity
no (por ejemplo, el contador pasa de 2 a 4), entonces hacemos cálculos innecesarios de la paridad y renderizaciones innecesarias.
¿Qué pasa si otra parte de nuestra interfaz de usuario simplemente quiere renderizarse cuando se actualiza el counter
?
¿Qué pasa si otra parte de nuestra interfaz de usuario depende únicamente de isEven
o parity
?
Incluso en este escenario relativamente simple, surgen rápidamente una serie de problemas. Podríamos intentar solucionar estos problemas introduciendo pub/sub para el counter
. Esto permitiría que consumidores adicionales del counter
pudieran suscribirse para agregar sus propias reacciones a los cambios de estado.
Sin embargo, todavía estamos atrapados con los siguientes problemas:
La función de renderizado, que solo depende de parity
debe "saber" que realmente necesita suscribirse al counter
.
No es posible actualizar la interfaz de usuario basándose únicamente en isEven
o parity
, sin interactuar directamente con counter
.
Hemos aumentado nuestro texto estándar. Cada vez que estás usando algo, no es sólo cuestión de llamar a una función o leer una variable, sino de suscribirte y realizar actualizaciones allí. Gestionar la baja también es especialmente complicado.
Ahora, podríamos resolver un par de problemas agregando pub/sub no solo al counter
sino también a isEven
y parity
. Entonces tendríamos que suscribirnos isEven
al counter
, parity
a isEven
y render
a parity
. Desafortunadamente, no solo nuestro código repetitivo ha explotado, sino que estamos atrapados con un montón de contabilidad de suscripciones y un posible desastre de pérdida de memoria si no limpiamos todo adecuadamente de la manera correcta. Entonces, resolvimos algunos problemas pero creamos una categoría de problemas completamente nueva y mucho código. Para empeorar las cosas, tenemos que pasar por todo este proceso para cada estado de nuestro sistema.
Las abstracciones de enlace de datos en las UI para el modelo y la vista han sido durante mucho tiempo fundamentales para los marcos de UI en múltiples lenguajes de programación, a pesar de la ausencia de dicho mecanismo integrado en JS o la plataforma web. Dentro de los marcos y bibliotecas de JS, se ha experimentado una gran cantidad de diferentes formas de representar este enlace, y la experiencia ha demostrado el poder del flujo de datos unidireccional junto con un tipo de datos de primera clase que representa una celda de estado o cálculo. derivados de otros datos, ahora llamados a menudo "Señales". Este enfoque de valor reactivo de primera clase parece haber hecho su primera aparición popular en marcos web JavaScript de código abierto con Knockout en 2010. En los años posteriores, se han creado muchas variaciones e implementaciones. En los últimos 3 o 4 años, los enfoques primitivos de Signal y relacionados han ganado mayor fuerza, y casi todas las bibliotecas o marcos de JavaScript modernos tienen algo similar, bajo un nombre u otro.
Para comprender las señales, echemos un vistazo al ejemplo anterior, reimaginado con una API de Signal que se articula con más detalle a continuación.
contador constante = nueva Signal.State(0);const esEven = nueva Signal.Computed(() => (counter.get() & 1) == 0);paridad constante = nueva Signal.Computed(() => esEven .get() ? "even" : "odd");// Una biblioteca o marco define efectos basados en otras primitivas de señaldeclare function effect(cb: () => void): (() => void);effect(( ) => element.innerText = parity.get());// Simular actualizaciones externas para counter...setInterval(() => counter.set(counter.get() + 1), 1000);
Hay algunas cosas que podemos ver de inmediato:
Hemos eliminado el ruido repetitivo alrededor de la variable counter
de nuestro ejemplo anterior.
Existe una API unificada para manejar valores, cálculos y efectos secundarios.
No hay ningún problema de referencia circular ni dependencias invertidas entre counter
y render
.
No hay suscripciones manuales ni necesidad de contabilidad.
Existe un medio para controlar el momento y la programación de los efectos secundarios.
Sin embargo, las señales nos brindan mucho más de lo que se puede ver en la superficie de la API:
Seguimiento automático de dependencias : una señal calculada descubre automáticamente cualquier otra señal de la que depende, ya sean valores simples u otros cálculos.
Evaluación diferida : los cálculos no se evalúan con entusiasmo cuando se declaran, ni se evalúan inmediatamente cuando cambian sus dependencias. Sólo se evalúan cuando se solicita explícitamente su valor.
Memoización : las señales computadas almacenan en caché su último valor para que los cálculos que no tienen cambios en sus dependencias no necesiten ser reevaluados, sin importar cuántas veces se acceda a ellos.
Cada implementación de Signal tiene su propio mecanismo de seguimiento automático, para realizar un seguimiento de las fuentes encontradas al evaluar una señal calculada. Esto dificulta compartir modelos, componentes y bibliotecas entre diferentes marcos; tienden a tener un acoplamiento falso con su motor de visualización (dado que las señales generalmente se implementan como parte de marcos JS).
Un objetivo de esta propuesta es desacoplar completamente el modelo reactivo de la vista de renderizado, permitiendo a los desarrolladores migrar a nuevas tecnologías de renderizado sin tener que reescribir su código que no sea UI, o desarrollar modelos reactivos compartidos en JS para implementarlos en diferentes contextos. Desafortunadamente, debido al control de versiones y la duplicación, ha resultado poco práctico alcanzar un alto nivel de uso compartido a través de bibliotecas de nivel JS; las funciones integradas ofrecen una mayor garantía de uso compartido.
Siempre es un pequeño aumento potencial de rendimiento enviar menos código debido a que se incorporan bibliotecas de uso común, pero las implementaciones de Signals son generalmente bastante pequeñas, por lo que no esperamos que este efecto sea muy grande.
Sospechamos que las implementaciones nativas de C++ de estructuras de datos y algoritmos relacionados con Signal pueden ser ligeramente más eficientes que lo que se puede lograr en JS, por un factor constante. Sin embargo, no se anticipan cambios algorítmicos en comparación con lo que estaría presente en un polyfill; No se espera que los motores sean mágicos aquí, y los propios algoritmos de reactividad estarán bien definidos e inequívocos.
El grupo campeón espera desarrollar varias implementaciones de Señales y utilizarlas para investigar estas posibilidades de rendimiento.
Con las bibliotecas de Signal existentes en lenguaje JS, puede resultar difícil rastrear cosas como:
La pila de llamadas a través de una cadena de señales calculadas, que muestra la cadena causal de un error.
El gráfico de referencia entre señales, cuando una depende de otra: importante al depurar el uso de memoria
Las señales integradas permiten que los tiempos de ejecución de JS y DevTools tengan potencialmente un soporte mejorado para inspeccionar señales, particularmente para la depuración o el análisis de rendimiento, ya sea que esté integrado en los navegadores o mediante una extensión compartida. Las herramientas existentes, como el inspector de elementos, la instantánea de rendimiento y los perfiladores de memoria, podrían actualizarse para resaltar específicamente las señales en su presentación de información.
En general, JavaScript ha tenido una biblioteca estándar bastante mínima, pero una tendencia en TC39 ha sido hacer de JS un lenguaje más "con baterías", con un conjunto de funcionalidades incorporadas de alta calidad disponibles. Por ejemplo, Temporal está reemplazando a moment.js, y una serie de características pequeñas, por ejemplo, Array.prototype.flat
y Object.groupBy
están reemplazando muchos casos de uso de lodash. Los beneficios incluyen tamaños de paquetes más pequeños, estabilidad y calidad mejoradas, menos que aprender al unirse a un nuevo proyecto y un vocabulario generalmente común entre los desarrolladores de JS.
El trabajo actual en el W3C y por parte de los implementadores de navegadores busca llevar plantillas nativas a HTML (partes DOM y creación de instancias de plantillas). Además, el CG de Componentes Web del W3C está explorando la posibilidad de ampliar los Componentes Web para ofrecer una API HTML totalmente declarativa. Para lograr ambos objetivos, eventualmente HTML necesitará una primitiva reactiva. Además, la comunidad puede imaginar y ha solicitado muchas mejoras ergonómicas en el DOM mediante la integración de señales.
Tenga en cuenta que esta integración sería un esfuerzo independiente que se realizará más adelante, no formará parte de esta propuesta en sí.
Los esfuerzos de estandarización a veces pueden resultar útiles sólo a nivel de "comunidad", incluso sin cambios en los navegadores. El esfuerzo de Signals está reuniendo a muchos autores de marcos diferentes para una discusión profunda sobre la naturaleza de la reactividad, los algoritmos y la interoperabilidad. Esto ya ha resultado útil y no justifica su inclusión en motores y navegadores JS; Las señales solo deben agregarse al estándar JavaScript si existen beneficios significativos más allá del intercambio de información del ecosistema habilitado.
Resulta que las bibliotecas de Signal existentes no son tan diferentes entre sí, en esencia. Esta propuesta tiene como objetivo aprovechar su éxito implementando las importantes cualidades de muchas de esas bibliotecas.
Un tipo de señal que representa el estado, es decir, señal grabable. Este es un valor que otros pueden leer.
Un tipo de señal calculada/memoria/derivada, que depende de otros y se calcula y almacena en caché de forma diferida.
La computación es diferida, lo que significa que las señales calculadas no se vuelven a calcular de forma predeterminada cuando una de sus dependencias cambia, sino que solo se ejecutan si alguien realmente las lee.
El cálculo está "libre de fallos", lo que significa que nunca se realizan cálculos innecesarios. Esto implica que, cuando una aplicación lee una señal calculada, hay una clasificación topológica de las partes potencialmente sucias del gráfico que se van a ejecutar, para eliminar cualquier duplicado.
El cálculo se almacena en caché, lo que significa que si, después de la última vez que cambia una dependencia, no ha cambiado ninguna dependencia, entonces la señal calculada no se vuelve a calcular cuando se accede.
Es posible realizar comparaciones personalizadas para las señales calculadas, así como para las señales de estado, para observar cuándo se deben actualizar otras señales calculadas que dependen de ellas.
Las reacciones a la condición en la que una señal calculada tiene una de sus dependencias (o dependencias anidadas) se vuelven "sucias" y cambian, lo que significa que el valor de la señal podría estar desactualizado.
Esta reacción tiene como objetivo programar trabajos más importantes que se realizarán más adelante.
Los efectos se implementan en términos de estas reacciones, además de la programación a nivel de marco.
Las señales computadas necesitan la capacidad de reaccionar si están registradas como una dependencia (anidada) de una de estas reacciones.
Habilite los marcos JS para realizar su propia programación. Sin programación forzada incorporada estilo Promise.
Se necesitan reacciones sincrónicas para permitir la programación del trabajo posterior basado en la lógica del marco.
Las escrituras son sincrónicas y surten efecto inmediatamente (un marco en el que las escrituras por lotes pueden hacer eso además).
Es posible separar la comprobación de si un efecto puede estar "sucio" de la ejecución real del efecto (habilitando un programador de efectos de dos etapas).
Capacidad de leer señales sin activar el registro de dependencias ( untrack
)
Habilite la composición de diferentes bases de código que utilizan señales/reactividad, por ejemplo,
Usar múltiples marcos juntos en lo que respecta al seguimiento/reactividad en sí (omisiones de módulo, ver más abajo)
Estructuras de datos reactivas independientes del marco (por ejemplo, proxy de almacenamiento reactivo recursivamente, mapa y conjunto y matriz reactivos, etc.)
Desalentar/prohibir el mal uso ingenuo de reacciones sincrónicas.
Riesgo de solidez: puede exponer "fallos" si se usa incorrectamente: si la renderización se realiza inmediatamente cuando se establece una señal, puede exponer el estado incompleto de la aplicación al usuario final. Por lo tanto, esta función solo debe usarse para programar de manera inteligente el trabajo para más adelante, una vez finalizada la lógica de la aplicación.
Solución: no permitir la lectura ni escritura de ninguna señal desde una devolución de llamada de reacción sincrónica
Desalentar untrack
y marcar su carácter no sólido
Riesgo de solidez: permite la creación de Señales calculadas cuyo valor depende de otras Señales, pero que no se actualizan cuando esas Señales cambian. Debe usarse cuando los accesos no rastreados no cambiarán el resultado del cálculo.
Solución: La API está marcada como "insegura" en el nombre.
Nota: Esta propuesta permite leer y escribir señales a partir de señales calculadas y de efectos, sin restringir las escrituras que vienen después de las lecturas, a pesar del riesgo de solidez. Esta decisión se tomó para preservar la flexibilidad y la compatibilidad en la integración con los marcos.
Debe ser una base sólida para que múltiples marcos implementen sus mecanismos de señales/reactividad.
Debería ser una buena base para proxies de almacenamiento recursivos, reactividad de campos de clases basados en decoradores y API de estilo .value
y [state, setState]
.
La semántica es capaz de expresar los patrones válidos habilitados por diferentes marcos. Por ejemplo, debería ser posible que estas señales sean la base de escrituras reflejadas inmediatamente o escrituras que se agrupan y aplican más tarde.
Sería bueno que los desarrolladores de JavaScript pudieran utilizar esta API directamente.
Idea: proporcione todos los ganchos, pero, si es posible, incluya errores cuando se utilicen incorrectamente.
Idea: colocar API sutiles en un espacio de nombres subtle
, similar a crypto.subtle
, para marcar la línea entre las API que son necesarias para un uso más avanzado, como implementar un marco o crear herramientas de desarrollo, versus un uso de desarrollo de aplicaciones más cotidiano, como crear instancias de señales para usar con un estructura.
Sin embargo, es importante no seguir literalmente los mismos nombres.
Si una característica coincide con un concepto de ecosistema, es bueno utilizar un vocabulario común.
Tensión entre "usabilidad por parte de desarrolladores de JS" y "proporcionar todos los ganchos a los marcos"
Ser implementable y utilizable con buen rendimiento: la API de superficie no genera demasiada sobrecarga.
Habilite las subclases para que los marcos puedan agregar sus propios métodos y campos, incluidos los campos privados. Esto es importante para evitar la necesidad de asignaciones adicionales a nivel del marco. Consulte "Administración de memoria" a continuación.
Si es posible: una señal calculada debe poder ser recolectada como basura si no hay nada activo que haga referencia a ella para posibles lecturas futuras, incluso si está vinculada a un gráfico más amplio que permanece activo (por ejemplo, leyendo un estado que permanece activo).
Tenga en cuenta que la mayoría de los marcos actuales requieren la eliminación explícita de las señales calculadas si tienen alguna referencia hacia o desde otro gráfico de señales que permanece vivo.
Esto no termina siendo tan malo cuando su vida útil está ligada a la vida útil de un componente de la interfaz de usuario y los efectos deben eliminarse de todos modos.
Si es demasiado costoso ejecutarlo con esta semántica, entonces deberíamos agregar la eliminación explícita (o "desvinculación") de las señales calculadas a la API a continuación, que actualmente carece de ella.
Un objetivo relacionado separado: minimizar el número de asignaciones, por ejemplo,
para hacer una señal grabable (evite dos cierres separados + matriz)
implementar efectos (evitar un cierre para cada reacción)
En la API para observar cambios de señal, evite crear estructuras de datos temporales adicionales.
Solución: API basada en clases que permite la reutilización de métodos y campos definidos en subclases
A continuación se muestra una idea inicial de una API de Signal. Tenga en cuenta que este es solo un borrador inicial y anticipamos cambios con el tiempo. Comencemos con los .d.ts
completos para tener una idea de la forma general y luego discutiremos los detalles de lo que significa.
interfaz Signal<T> {// Obtener el valor de signalget(): T;}namespace Signal {// Una clase de señal de lectura y escritura State<T> implementa Signal<T> {// Crea una señal de estado comenzando con el valor tconstructor(t: T, options?: SignalOptions<T>);// Obtener el valor de la señalget(): T;// Establecer el valor de la señal de estado en tset(t: T): void;}// Una señal que es una fórmula basada en otras Signalsclass Computed<T = desconocido> implementa Signal<T> {// Crea una señal que evalúa el valor devuelto por la devolución de llamada.// La devolución de llamada se llama con esta señal como este value.constructor(cb: (this: Computed< T>) => T, opciones?: SignalOptions<T>);// Obtener el valor de signalget(): T;}// Este espacio de nombres incluye características "avanzadas" que es mejor// dejar para los autores del marco. que la aplicación desarrolladores.// Análogo a `crypto.subtle`namespace sutil {// Ejecutar una devolución de llamada con todo el seguimiento deshabilitadofunción untrack<T>(cb: () => T): T;// Obtener la señal calculada actual que está rastreando cualquier la señal lee, si hay alguna función currentComputed(): Computed | null;// Devuelve una lista ordenada de todas las señales a las que hizo referencia // durante la última vez que se evaluó.// Para un observador, enumera el conjunto de señales que está observando.función introspectSources(s: Computed | Watcher): (Estado | Computado)[];// Devuelve los observadores que contienen esta señal, más cualquier// Señales calculadas que leyeron esta señal la última vez que fueron evaluadas,// si esa señal calculada se observa (recursivamente). introspectSinks(s: Estado | Computado): (Computado | Vigilante)[];// Verdadero si esta señal está "en vivo", en el sentido de que es observada por un Vigilante,// o es leída por una señal Computada que es ( recursivamente) live.function hasSinks(s: State | Computed): boolean;// Verdadero si este elemento es "reactivo", en el sentido de que depende// de alguna otra señal. Un Calculado donde hasSources es falso// siempre devolverá la misma constante. Función hasSources(s: Computed | Watcher): boolean;class Watcher {// Cuando se escribe en una fuente (recursiva) de Watcher, llama a esta devolución de llamada,// si aún no ha sido llamado desde la última llamada `watch`.// No se pueden leer ni escribir señales durante notify.constructor(notify: (this: Watcher) => void);// Agregue estos envía señales al conjunto del Vigilante y configura el observador para que ejecute su // notificación de devolución de llamada la próxima vez que cambie cualquier señal en el conjunto (o una de sus dependencias). // Se puede llamar sin argumentos solo para restablecer el estado "notificado", para que// la devolución de llamada de notificación se invoque nuevamente.watch(...s: Signal[]): void;// Elimina estas señales del conjunto observado (por ejemplo, para un efecto que se elimina)unwatch(... s: Señal[]): void;// Devuelve el conjunto de fuentes en el conjunto del Vigilante que aún están sucias, o es una señal calculada// con una fuente que está sucia o pendiente y aún no ha sido reevaluadagetPending(): Signal[];} // Ganchos para observar si se está mirando o ya no se está viendovar visto: Símbolo;var unwatched: Símbolo;}interfaz SignalOptions<T> {// Función de comparación personalizada entre el valor antiguo y el nuevo. Valor predeterminado: Object.is.// La señal se pasa como este valor para context.equals?: (this: Signal<T>, t: T, t2: T) => boolean;// Se llama a la devolución de llamada cuando isWatched se convierte verdadero, si anteriormente era falso[Signal.subtle.watched]?: (this: Signal<T>) => void;// La devolución de llamada llamada cada vez que isWatched se vuelve falsa, si era previamente verdadero[Señal.subtle.unwatched]?: (esto: Señal<T>) => void;}}
Una señal representa una celda de datos que puede cambiar con el tiempo. Las señales pueden ser de "estado" (sólo un valor que se establece manualmente) o "calculadas" (una fórmula basada en otras señales).
Las señales calculadas funcionan rastreando automáticamente qué otras señales se leen durante su evaluación. Cuando se lee un cálculo, verifica si alguna de sus dependencias registradas previamente ha cambiado y, de ser así, se reevalúa a sí mismo. Cuando se anidan varias señales calculadas, toda la atribución del seguimiento va a la más interna.
Las señales computadas son diferidas, es decir, basadas en extracción: sólo se reevalúan cuando se accede a ellas, incluso si una de sus dependencias cambió antes.
La devolución de llamada pasada a las señales calculadas generalmente debe ser "pura" en el sentido de ser una función determinista y libre de efectos secundarios de las otras señales a las que accede. Al mismo tiempo, el momento en que se realiza la devolución de llamada es determinista, lo que permite utilizar los efectos secundarios con cuidado.
Las señales cuentan con almacenamiento en caché/memorización destacado: tanto las señales estatales como las calculadas recuerdan su valor actual y solo activan el recálculo de las señales calculadas que hacen referencia a ellas si realmente cambian. Ni siquiera es necesaria una comparación repetida de los valores antiguos con los nuevos: la comparación se realiza una vez cuando la señal de origen se restablece/reevalúa, y el mecanismo de señal realiza un seguimiento de qué elementos que hacen referencia a esa señal no se han actualizado en función de los nuevos. valor todavía. Internamente, esto generalmente se representa mediante "coloración de gráficos" como se describe en (publicación del blog de Milo).
Las señales computadas rastrean sus dependencias dinámicamente: cada vez que se ejecutan, pueden terminar dependiendo de diferentes cosas, y ese conjunto de dependencias preciso se mantiene actualizado en el gráfico de señales. Esto significa que si necesita una dependencia en solo una rama y el cálculo anterior tomó la otra rama, entonces un cambio en ese valor no utilizado temporalmente no hará que se vuelva a calcular la señal calculada, incluso cuando se extraiga.
A diferencia de JavaScript Promises, todo en Signals se ejecuta de forma sincrónica:
Establecer una señal en un nuevo valor es sincrónico y esto se refleja inmediatamente al leer cualquier señal calculada que dependa de ella posteriormente. No existe un procesamiento por lotes incorporado de esta mutación.
La lectura de señales calculadas es sincrónica: su valor siempre está disponible.
La devolución de llamada notify
en Watchers, como se explica a continuación, se ejecuta sincrónicamente, durante la llamada .set()
que la desencadenó (pero después de que se haya completado el color del gráfico).
Al igual que las promesas, las señales pueden representar un estado de error: si se produce una devolución de llamada de una señal calculada, ese error se almacena en caché como cualquier otro valor y se vuelve a generar cada vez que se lee la señal.
Una instancia Signal
representa la capacidad de leer un valor que cambia dinámicamente cuyas actualizaciones se rastrean a lo largo del tiempo. También incluye implícitamente la capacidad de suscribirse a la Señal, implícitamente a través de un acceso rastreado desde otra Señal calculada.
La API aquí está diseñada para coincidir con el consenso aproximado del ecosistema entre una gran fracción de bibliotecas de Signal en el uso de nombres como "señal", "calculado" y "estado". Sin embargo, el acceso a las señales computadas y de estado se realiza a través de un método .get()
, que no está de acuerdo con todas las API de señales populares, que utilizan un descriptor de acceso estilo .value
o una sintaxis de llamada signal()
.
La API está diseñada para reducir la cantidad de asignaciones, para que Signals sea adecuada para integrarse en marcos de JavaScript y al mismo tiempo alcanzar el mismo o mejor rendimiento que las señales personalizadas de marcos existentes. Esto implica:
Las señales de estado son un único objeto grabable, al que se puede acceder y configurar desde la misma referencia. (Consulte las implicaciones a continuación en la sección "Separación de capacidades").
Tanto las señales de estado como las señalizadas computadas están diseñadas para ser subclasificables, para facilitar la capacidad de los marcos de agregar propiedades adicionales a través de campos de clase públicos y privados (así como métodos para usar ese estado).
Se llaman varias devoluciones de llamada (por ejemplo, equals
a la devolución de llamada calculada) con la señal relevante como this
valor para el contexto, de modo que no se necesita un nuevo cierre por señal. En cambio, el contexto se puede guardar en propiedades adicionales de la propia señal.
Algunas condiciones de error impuestas por esta API:
Es un error leer un calculado de forma recursiva.
La devolución de llamada notify
de un Vigilante no puede leer ni escribir ninguna señal.
Si se produce una devolución de llamada de Signal calculada, los accesos posteriores a Signal vuelven a generar ese error almacenado en caché, hasta que una de las dependencias cambia y se vuelve a calcular.
Algunas condiciones que no se aplican:
Las señales calculadas pueden escribir en otras señales, de forma sincrónica dentro de su devolución de llamada.
El trabajo que está en cola mediante la devolución de llamada notify
de un Vigilante puede leer o escribir señales, lo que hace posible replicar los antipatrones clásicos de React en términos de Señales.
La interfaz Watcher
definida anteriormente proporciona la base para implementar API JS típicas para efectos: devoluciones de llamada que se vuelven a ejecutar cuando otras señales cambian, únicamente por su efecto secundario. La función effect
utilizada anteriormente en el ejemplo inicial se puede definir de la siguiente manera:
// Esta función normalmente viviría en una biblioteca/marco, no en el código de la aplicación// NOTA: Esta lógica de programación es demasiado básica para ser útil. No copiar/pegar.let pendiente = false;let w = new Signal.subtle.Watcher(() => {if (!pending) {pending = true;queueMicrotask(() => {pending = false;for (let s of w.getPending()) s.get();w.watch();});}});// Un efecto de efecto Señal que se evalúa como cb, que programa una lectura de // sí mismo en la cola de microtask siempre que uno de sus las dependencias pueden cambiar efecto de la función de exportación (cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch(c);c.get ();return () => { destructor?.(); w.unwatch(c) };}
La API Signal no incluye ninguna función integrada como effect
. Esto se debe a que la programación de efectos es sutil y, a menudo, se vincula con los ciclos de renderizado del marco y otros estados o estrategias específicos del marco de alto nivel a los que JS no tiene acceso.
Repasando las diferentes operaciones utilizadas aquí: La devolución de llamada notify
pasada al constructor Watcher
es la función que se llama cuando la señal pasa de un estado "limpio" (donde sabemos que el caché está inicializado y es válido) a un estado "comprobado" o "sucio". "estado (donde el caché puede o no ser válido porque al menos uno de los estados del que depende recursivamente ha sido cambiado).
Las llamadas para notify
se activan en última instancia mediante una llamada a .set()
en algún estado de Señal. Esta llamada es sincrónica: ocurre antes de que regrese .set
. Pero no hay necesidad de preocuparse de que esta devolución de llamada observe el gráfico de señal en un estado medio procesado, porque durante una devolución de llamada notify
, no se puede leer ni escribir ninguna señal, incluso en una llamada untrack
. Debido a que se llama notify
durante .set()
, está interrumpiendo otro hilo de lógica, que podría no estar completo. Para leer o escribir señales desde notify
, programe el trabajo para que se ejecute más tarde, por ejemplo, escribiendo la señal en una lista para acceder a ella más tarde, o con queueMicrotask
como se indicó anteriormente.
Tenga en cuenta que es perfectamente posible utilizar Señales de manera efectiva sin Symbol.subtle.Watcher
programando el sondeo de Señales calculadas, como lo hace Glimmer. Sin embargo, muchos marcos han descubierto que a menudo es útil que esta lógica de programación se ejecute de forma sincrónica, por lo que la API de Signals la incluye.
Tanto las señales calculadas como las de estado se recolectan como cualquier valor JS. Pero los Vigilantes tienen una forma especial de mantener vivas las cosas: cualquier señal que sea observada por un Vigilante se mantendrá viva siempre que cualquiera de los estados subyacentes sea accesible, ya que estos pueden desencadenar una futura llamada notify
(y luego un futuro .get()
). Por este motivo, recuerde llamar Watcher.prototype.unwatch
para limpiar los efectos.
Signal.subtle.untrack
es una trampilla de escape que permite leer señales sin rastrear esas lecturas. Esta capacidad no es segura porque permite la creación de Señales calculadas cuyo valor depende de otras Señales, pero que no se actualizan cuando esas Señales cambian. Debe usarse cuando los accesos no rastreados no cambiarán el resultado del cálculo.
Estas características pueden agregarse más adelante, pero no están incluidas en el borrador actual. Su omisión se debe a la falta de consenso establecido en el espacio de diseño entre los marcos, así como a la capacidad demostrada para solucionar su ausencia con mecanismos además de la noción de Señales descrita en este documento. Sin embargo, lamentablemente, la omisión limita el potencial de interoperabilidad entre marcos. A medida que se produzcan prototipos de señales como se describe en este documento, se hará un esfuerzo para reexaminar si estas omisiones fueron la decisión adecuada.
Asíncrono : en este modelo, las señales siempre están disponibles de forma sincrónica para su evaluación. Sin embargo, con frecuencia es útil tener ciertos procesos asincrónicos que conducen a la configuración de una señal y comprender cuándo una señal todavía se está "cargando". Una forma sencilla de modelar el estado de carga es con excepciones, y el comportamiento de almacenamiento en caché de excepciones de las señales calculadas se compone de manera bastante razonable con esta técnica. Las técnicas mejoradas se analizan en el número 30.
Transacciones : para las transiciones entre vistas, suele ser útil mantener un estado activo tanto para el estado "desde" como para el estado "hacia". El estado "a" se representa en segundo plano, hasta que está listo para intercambiar (confirmar la transacción), mientras que el estado "desde" permanece interactivo. Mantener ambos estados al mismo tiempo requiere "bifurcar" el estado del gráfico de señal, e incluso puede resultar útil admitir múltiples transiciones pendientes a la vez. Discusión en el número 73.
También se omiten algunos posibles métodos de conveniencia.
Esta propuesta está en la agenda del TC39 de abril de 2024 para la Etapa 1. Actualmente se puede considerar como "Etapa 0".
Está disponible un polyfill para esta propuesta, con algunas pruebas básicas. Algunos autores de marcos han comenzado a experimentar con la sustitución de esta implementación de señal, pero este uso se encuentra en una etapa inicial.
Los colaboradores de la propuesta de Signal quieren ser especialmente conservadores en la forma en que impulsamos esta propuesta, para no caer en la trampa de enviar algo de lo que terminemos arrepintiéndonos y sin utilizar. Nuestro plan es realizar las siguientes tareas adicionales, no requeridas por el proceso TC39, para asegurarnos de que esta propuesta vaya por buen camino:
Antes de proponer la Etapa 2, planeamos:
Desarrolle múltiples implementaciones de polyfill de nivel de producción que sean sólidas, bien probadas (por ejemplo, que pasen pruebas de varios marcos, así como pruebas de estilo test262) y competitivas en términos de rendimiento (como se verifica con un conjunto exhaustivo de puntos de referencia de señal/marco).
Integre la API Signal propuesta en una gran cantidad de marcos JS que consideramos algo representativos, y algunas aplicaciones grandes funcionan con esta base. Pruebe que funciona de manera eficiente y correcta en estos contextos.
Tener un conocimiento sólido sobre el espacio de posibles extensiones de la API y haber concluido cuáles (si corresponde) deberían agregarse a esta propuesta.
Esta sección describe cada una de las API expuestas a JavaScript, en términos de los algoritmos que implementan. Esto puede considerarse como una protoespecificación y se incluye en este punto inicial para concretar un posible conjunto de semánticas, al mismo tiempo que está muy abierto a cambios.
Algunos aspectos del algoritmo:
El orden de las lecturas de señales dentro de un computado es significativo y se puede observar en el orden en que se ejecutan ciertas devoluciones de llamada (que Watcher
se invoca, equals
, el primer parámetro de new Signal.Computed
y las devoluciones de llamada watched
/ unwatched
). Esto significa que las fuentes de una señal calculada deben almacenarse ordenadas.
Estas cuatro devoluciones de llamada pueden generar excepciones, y estas excepciones se propagan de manera predecible al código JS que llama. Las excepciones no detienen la ejecución de este algoritmo ni dejan el gráfico a medio procesar. Para los errores generados en la devolución de llamada notify
de un Watcher, esa excepción se envía a la llamada .set()
que la desencadenó, usando un AggregateError si se lanzaron múltiples excepciones. Los demás (¿incluidos watched
/ unwatched
?) se almacenan en el valor de la Señal, para volver a lanzarse cuando se lea, y dicha Señal relanzada se puede marcar como ~clean~
como cualquier otra con un valor normal.
Se tiene cuidado de evitar circularidades en los casos de señales calculadas que no son "observadas" (siendo observadas por ningún Vigilante), de modo que puedan recolectarse basura independientemente de otras partes del gráfico de señales. Internamente, esto se puede implementar con un sistema de números de generación que siempre se recopilan; tenga en cuenta que las implementaciones optimizadas también pueden incluir números de generación locales por nodo o evitar el seguimiento de algunos números en las señales observadas.
Los algoritmos de señal deben hacer referencia a cierto estado global. Este estado es global para todo el subproceso o "agente".
computing
: La señal calculada o de efecto más interna que actualmente se está reevaluando debido a una llamada .get
o .run
, o null
. Inicialmente null
.
frozen
: booleano que indica si actualmente se está ejecutando una devolución de llamada que requiere que no se modifique el gráfico. Inicialmente false
.
generation
: un número entero incremental, que comienza en 0, que se utiliza para realizar un seguimiento de la actualidad de un valor evitando circularidades.
Signal
Signal
es un objeto ordinario que sirve como espacio de nombres para clases y funciones relacionadas con Signal.
Signal.subtle
es un objeto de espacio de nombres interno similar.
Signal.State
Signal.State
value
: El valor actual de la señal de estado.
equals
: La función de comparación utilizada al cambiar valores
watched
: la devolución de llamada que se llamará cuando la señal sea observada por un efecto
unwatched
: la devolución de llamada que se llamará cuando un efecto ya no observe la señal.
sinks
: Conjunto de señales vigiladas que dependen de ésta.
Signal.State(initialValue, options)
Establezca value
de esta señal en initialValue
.
¿Establecer esta señal equals
a opciones? .equals
¿Establecer esta señal watched
en opciones? [Señal.subtil.vigilada]
¿Establecer esta señal unwatched
en opciones?.[Signal.subtle.unwatched]
Establezca sinks
de esta señal en el conjunto vacío
Signal.State.prototype.get()
Si frozen
es verdadero, lanza una excepción.
Si computing
no está undefined
, agregue esta señal al conjunto sources
de computing
.
NOTA: No agregamos computing
al conjunto de sinks
de esta señal hasta que sea observada por un Vigilante.
Devuelve value
de esta señal.
Signal.State.prototype.set(newValue)
Si el contexto de ejecución actual está frozen
, lanza una excepción.
Ejecute el algoritmo "establecer valor de señal" con esta señal y el primer parámetro para el valor.
Si ese algoritmo devolvió ~clean~
, entonces devuelve indefinido.
Establezca el state
de todos sinks
de esta Señal en (si es una Señal Computada) ~dirty~
si previamente estaban limpias, o (si es un Vigilante) ~pending~
si anteriormente estaba ~watching~
.
Establezca el state
de todas las dependencias de la señal computarizada de los sumideros (recursivamente) en ~checked~
si previamente estaban ~clean~
(es decir, deje las marcas sucias en su lugar), o para los Vigilantes, ~pending~
si previamente ~watching~
.
Para cada Vigilante ~watching~
previamente encontrado en esa búsqueda recursiva, luego en primer orden en profundidad,
Establecer frozen
en verdadero.
Llamar a su devolución de llamada notify
(guardando a un lado cualquier excepción lanzada, pero ignorando el valor de retorno de notify
).
Restaurar frozen
a falso.
Establezca el state
del Vigilante en ~waiting~
.
Si se produjo alguna excepción desde las devoluciones de llamada notify
, propáguela a la persona que llama después de que se hayan ejecutado todas las devoluciones de llamada notify
. Si hay varias excepciones, entonces empaquetarlas juntas en un AggregateError y lanzarlo.
Devuelve indefinido.
Signal.Computed
Signal.Computed
de estado calculada El state
de una Señal Computada puede ser uno de los siguientes:
~clean~
: El valor de la señal está presente y se sabe que no está obsoleto.
~checked~
: Una fuente (indirecta) de esta señal ha cambiado; Esta señal tiene un valor pero puede estar obsoleta. Sólo se sabrá si está obsoleto o no cuando se hayan evaluado todas las fuentes inmediatas.
~computing~
: La devolución de llamada de esta señal se está ejecutando actualmente como efecto secundario de una llamada .get()
.
~dirty~
: O esta señal tiene un valor que se sabe que está obsoleto o nunca ha sido evaluado.
El gráfico de transición es el siguiente:
stateDiagram-v2
[*] --> sucio
sucio --> informática: [4]
informática --> limpio: [5]
limpio --> sucio: [2]
limpio --> marcado: [3]
marcado --> limpio: [6]
marcado --> sucio: [1]
CargandoLas transiciones son:
Número | De | A | Condición | Algoritmo |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | Se ha evaluado una fuente inmediata de esta señal, que es una señal calculada, y su valor ha cambiado. | Algoritmo: recalcular la señal calculada sucia |
2 | ~clean~ | ~dirty~ | Se ha fijado una fuente inmediata de esta señal, que es un Estado, con un valor que no es igual a su valor anterior. | Método: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | Se ha fijado una fuente recursiva, pero no inmediata, de esta señal, que es un Estado, con un valor que no es igual a su valor anterior. | Método: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | Estamos a punto de ejecutar la callback . | Algoritmo: recalcular la señal calculada sucia |
5 | ~computing~ | ~clean~ | La callback terminó de evaluarse y devolvió un valor o generó una excepción. | Algoritmo: recalcular la señal calculada sucia |
6 | ~checked~ | ~clean~ | Todas las fuentes inmediatas de esta señal han sido evaluadas y todas se han descubierto sin cambios, por lo que ahora se sabe que no estamos obsoletos. | Algoritmo: recalcular la señal calculada sucia |
Signal.Computed
Ranuras internas calculadas value
: el valor almacenado en caché anterior de la señal, o ~uninitialized~
para una señal calculada nunca leída. El valor puede ser una excepción que se vuelve a generar cuando se lee el valor. Siempre undefined
para señales de efectos.
state
: Puede estar ~clean~
, ~checked~
, ~computing~
o ~dirty~
.
sources
: un conjunto ordenado de señales del que depende esta señal.
sinks
: un conjunto ordenado de señales que dependen de esta señal.
equals
: El método igual proporcionado en las opciones.
callback
: la devolución de llamada que se llama para obtener el valor de la señal calculada. Se establece en el primer parámetro pasado al constructor.
Signal.Computed
calculadoEl constructor establece
callback
a su primer parámetro
equals
según las opciones, por defecto es Object.is
si está ausente
state
a ~dirty~
value
a ~uninitialized~
Con AsyncContext, la devolución de llamada pasada a new Signal.Computed
cierra la instantánea desde cuando se llamó al constructor y restaura esta instantánea durante su ejecución.
Signal.Computed.prototype.get
Si el contexto de ejecución actual está frozen
o si esta Señal tiene el estado ~computing~
, o si esta señal es un Efecto y computing
una Señal calculada, lanza una excepción.
Si computing
no es null
, agregue esta señal al conjunto sources
de computing
.
NOTA: No agregamos computing
al conjunto de sinks
de esta señal hasta/a menos que sea observada por un Vigilante.
Si el estado de esta señal es ~dirty~
o ~checked~
: repita los siguientes pasos hasta que esta señal esté ~clean~
:
Recurra a través de sources
para encontrar la fuente recursiva más profunda, más a la izquierda (es decir, la más temprana observada), que es una señal computada marcada ~dirty~
(interrumpiendo la búsqueda al encontrar una señal computada ~clean~
e incluyendo esta señal computada como lo último para buscar).
Realice el algoritmo "recalcular la señal calculada sucia" en esa señal.
En este punto, el estado de esta señal será ~clean~
y ninguna fuente recursiva estará ~dirty~
o ~checked~
. Devuelve el value
de la señal. Si el valor es una excepción, vuelva a generar esa excepción.
Signal.subtle.Watcher
Signal.subtle.Watcher
El state
de un Vigilante puede ser uno de los siguientes:
~waiting~
: La devolución de llamada notify
se ha ejecutado o el Vigilante es nuevo, pero no está observando activamente ninguna señal.
~watching~
: El Vigilante está observando activamente las señales, pero aún no se han producido cambios que requieran una devolución de llamada notify
.
~pending~
: una dependencia del Vigilante ha cambiado, pero la devolución de llamada notify
aún no se ha ejecutado.
El gráfico de transición es el siguiente:
stateDiagram-v2
[*] --> esperando
esperando --> mirando: [1]
mirando --> esperando: [2]
viendo --> pendiente: [3]
pendiente --> esperando: [4]
CargandoLas transiciones son:
Número | De | A | Condición | Algoritmo |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | Se ha llamado al método watch del Vigilante. | Método: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | Se llamó al método unwatch del Vigilante y se eliminó la última señal observada. | Método: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | Es posible que una señal observada haya cambiado de valor. | Método: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | Se ha ejecutado la devolución de llamada notify . | Método: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
state
: Puede estar ~watching~
, ~pending~
o ~waiting~
signals
: un conjunto ordenado de señales que este observador está observando
notifyCallback
: la devolución de llamada que se llama cuando algo cambia. Se establece en el primer parámetro pasado al constructor.
new Signal.subtle.Watcher(callback)
state
está configurado en ~waiting~
.
Inicializa signals
como un conjunto vacío.
notifyCallback
está configurado en el parámetro de devolución de llamada.
Con AsyncContext, la devolución de llamada pasada al new Signal.subtle.Watcher
no se cierra sobre la instantánea de cuando se llamó al constructor, por lo que la información contextual alrededor de la escritura es visible.
Signal.subtle.Watcher.prototype.watch(...signals)
Si frozen
es verdadero, lanza una excepción.
Si alguno de los argumentos no es una señal, lanza una excepción.
Agregue todos los argumentos al final de signals
de este objeto.
Para cada señal recién vista, en orden de izquierda a derecha,
Agregue este observador como sink
de esa señal.
Si este fue el primer sumidero, recurra a las fuentes para agregar esa señal como sumidero.
Establecer frozen
en verdadero.
Llame a la devolución de llamada watched
si existe.
Restaurar frozen
a falso.
Si el state
de la señal es ~waiting~
, configúrelo en ~watching~
.
Signal.subtle.Watcher.prototype.unwatch(...signals)
Si frozen
es verdadero, lanza una excepción.
Si alguno de los argumentos no es una señal o no está siendo observado por este observador, lanza una excepción.
Para cada señal en los argumentos, en orden de izquierda a derecha,
Elimina esa señal del conjunto signals
de este Vigilante.
Elimina este Vigilante del conjunto de sink
de esa Señal.
Si el conjunto sink
de esa señal se ha quedado vacío, elimine esa señal como sumidero de cada una de sus fuentes.
Establecer frozen
en verdadero.
Llame a la devolución de llamada unwatched
si existe.
Restaurar frozen
a falso.
Si el observador ahora no tiene signals
y su state
es ~watching~
, configúrelo en ~waiting~
.
Signal.subtle.Watcher.prototype.getPending()
Devuelve una matriz que contiene el subconjunto de signals
que son señales calculadas en los estados ~dirty~
o ~pending~
.
Signal.subtle.untrack(cb)
Sea c
el estado computing
actual del contexto de ejecución.
Establezca computing
en nulo.
Llame cb
.
Restaure computing
en c
(incluso si cb
lanzó una excepción).
Devuelve el valor de retorno de cb
(vuelve a lanzar cualquier excepción).
Nota: el desbloqueo no le saca del estado frozen
, que se mantiene estrictamente.
Signal.subtle.currentComputed()
Devuelve el valor computing
actual.
Borre el conjunto sources
de esta señal y elimínelo de los conjuntos de sinks
de esas fuentes.
Guarde el valor computing
anterior y configure computing
en esta señal.
Establezca el estado de esta señal en ~computing~
.
Ejecute la devolución de llamada de esta señal calculada, utilizando esta señal como este valor. Guarde el valor de retorno y, si la devolución de llamada generó una excepción, guárdela para volver a generarla.
Restaura el valor computing
anterior.
Aplique el algoritmo "establecer valor de señal" al valor de retorno de la devolución de llamada.
Establece el estado de esta señal en ~clean~
.
Si ese algoritmo devolvió ~dirty~
: marque todos los sumideros de esta señal como ~dirty~
(anteriormente, los sumideros pueden haber sido una mezcla de marcados y sucios). (O, si esto no se vigila, entonces adopte un número de nueva generación para indicar suciedad, o algo así).
De lo contrario, ese algoritmo devolvió ~clean~
: En este caso, para cada sumidero ~checked~
de esta señal, si todas las fuentes de esa señal ahora están limpias, entonces márquela como ~clean~
también. Aplique este paso de limpieza a más sumideros de forma recursiva, a cualquier señal recién limpia que haya verificado sumideros. (O, si esto no se observa, indique lo mismo de alguna manera, para que la limpieza pueda continuar con pereza).
Si a este algoritmo se le pasó un valor (a diferencia de una excepción para volver a lanzar, del algoritmo de recalcular señal calculada sucia):
Llame a la función equals
de esta Señal, pasando como parámetros el value
actual, el nuevo valor y esta Señal. Si se genera una excepción, guárdela (para volver a generarla cuando se lea) como el valor de la señal y continúe como si la devolución de llamada hubiera devuelto falso.
Si esa función devolvió verdadero, devuelve ~clean~
.
Establezca el value
de esta señal en el parámetro.
Volver ~dirty~
P : ¿No es un poco pronto para estandarizar algo relacionado con las señales, cuando recién comenzaron a ser la novedad de moda en 2022? ¿No deberíamos darles más tiempo para evolucionar y estabilizarse?
R : El estado actual de Signals en los frameworks web es el resultado de más de 10 años de desarrollo continuo. A medida que aumenta la inversión, como lo ha hecho en los últimos años, casi todos los marcos web se están acercando a un modelo central de señales muy similar. Esta propuesta es el resultado de un ejercicio de diseño compartido entre un gran número de líderes actuales en marcos web, y no avanzará hacia la estandarización sin la validación de ese grupo de expertos en el dominio en diversos contextos.
P : ¿Pueden los marcos incluso utilizar las señales integradas, dada su estrecha integración con el renderizado y la propiedad?
R : Las partes que son más específicas del marco tienden a estar en el área de efectos, programación y propiedad/disposición, que esta propuesta no intenta resolver. Nuestra primera prioridad al crear prototipos de señales de seguimiento de estándares es validar que puedan ubicarse "debajo" de los marcos existentes de manera compatible y con buen rendimiento.
P : ¿La API Signal está pensada para que la utilicen directamente los desarrolladores de aplicaciones o para que la utilicen marcos de trabajo?
R : Si bien esta API podría ser utilizada directamente por los desarrolladores de aplicaciones (al menos la parte que no está dentro del espacio de nombres Signal.subtle
), no está diseñada para ser especialmente ergonómica. En cambio, las necesidades de los autores de bibliotecas/marcos son prioridades. Se espera que la mayoría de los marcos incluyan incluso las API básicas Signal.State
y Signal.Computed
con algo que exprese su inclinación ergonómica. En la práctica, normalmente es mejor usar Signals a través de un marco, que gestiona funciones más complicadas (p. ej., Watcher, untrack
), además de gestionar la propiedad y la eliminación (p. ej., determinar cuándo se deben agregar y eliminar señales de los observadores), y programación de renderizado a DOM: esta propuesta no intenta resolver esos problemas.
P : ¿Tengo que eliminar las señales relacionadas con un widget cuando ese widget se destruye? ¿Cuál es la API para eso?
R : La operación de desmontaje relevante aquí es Signal.subtle.Watcher.prototype.unwatch
. Solo las señales observadas deben limpiarse (dejándolas de mirar), mientras que las señales no observadas se pueden recolectar basura automáticamente.
P : ¿Las señales funcionan con VDOM o directamente con el DOM HTML subyacente?
R : ¡Sí! Las señales son independientes de la tecnología de renderizado. Los marcos de JavaScript existentes que utilizan construcciones similares a Signal se integran con VDOM (por ejemplo, Preact), el DOM nativo (por ejemplo, Solid) y una combinación (por ejemplo, Vue). Lo mismo será posible con las señales integradas.
P : ¿Será ergonómico usar Signals en el contexto de marcos basados en clases como Angular y Lit? ¿Qué pasa con los marcos basados en compiladores como Svelte?
R : Los campos de clase se pueden hacer basados en Signal con un decorador de acceso simple, como se muestra en el archivo Léame de Polyfill de Signal. Las señales están muy alineadas con las Runas de Svelte 5: es sencillo para un compilador transformar runas a la API Signal definida aquí y, de hecho, esto es lo que hace Svelte 5 internamente (pero con su propia biblioteca de Señales).
P : ¿Las señales funcionan con SSR? ¿Hidratación? ¿Reanudabilidad?
R : Sí. Qwik utiliza Signals con buenos resultados con ambas propiedades, y otros marcos tienen otros enfoques bien desarrollados para la hidratación con Signals con diferentes compensaciones. Creemos que es posible modelar las señales reanudables de Qwik utilizando una señal de estado y una señal calculada conectadas entre sí, y planeamos demostrarlo en código.
P : ¿Las señales funcionan con un flujo de datos unidireccional como lo hace React?
R : Sí, las señales son un mecanismo para el flujo de datos unidireccional. Los marcos de interfaz de usuario basados en señales le permiten expresar su punto de vista en función del modelo (donde el modelo incorpora señales). Un gráfico de estado y señales calculadas es acíclico por construcción. También es posible recrear antipatrones de React dentro de Signals (!), por ejemplo, el equivalente de Signal de un setState
dentro de useEffect
es usar un Watcher para programar una escritura en una señal de State.
P : ¿Cómo se relacionan las señales con los sistemas de gestión estatal como Redux? ¿Las señales fomentan el estado desestructurado?
R : Las señales pueden formar una base eficiente para abstracciones de gestión de estado tipo tienda. Un patrón común que se encuentra en múltiples marcos es un objeto basado en un Proxy que representa internamente propiedades usando señales, por ejemplo, Vue reactive()
o tiendas Solid. Estos sistemas permiten una agrupación flexible de estados en el nivel correcto de abstracción para la aplicación particular.
P : ¿Qué ofrecen Signals que Proxy
no maneja actualmente?
R : Los proxy y las señales son complementarios y combinan bien. Los proxies le permiten interceptar operaciones de objetos superficiales y las señales coordinan un gráfico de dependencia (de celdas). Respaldar un Proxy con señales es una excelente manera de crear una estructura reactiva anidada con gran ergonomía.
En este ejemplo, podemos usar un proxy para hacer que la señal tenga una propiedad getter y setter en lugar de usar los métodos get
y set
:
const a = nueva Señal.Estado(0);const b = nuevo Proxy(a, { get(objetivo, propiedad, receptor) {if (propiedad === 'valor') { return target.get():} } set(objetivo, propiedad, valor, receptor) {if (propiedad === 'valor') { target.set(valor)!} }});// uso en un contexto reactivo hipotético:<plantilla> {b.valor} <botón al hacer clic={() => {b.valor++; }}>cambiar</button></template>
cuando se utiliza un renderizador optimizado para una reactividad detallada, al hacer clic en el botón se actualizará la celda b.value
.
Ver:
ejemplos de estructuras reactivas anidadas creadas con señales y proxies: signal-utils
Ejemplos de implementaciones anteriores que muestran la relación entre los datos reactivos y los proxies: integrados con seguimiento.
discusión.
P : ¿Las señales se basan en push o pull?
R : La evaluación de las señales calculadas se basa en extracción: las señales calculadas solo se evalúan cuando se llama a .get()
, incluso si el estado subyacente cambió mucho antes. Al mismo tiempo, cambiar una señal de estado puede activar inmediatamente la devolución de llamada de un Vigilante, "impulsando" la notificación. Por lo tanto, se puede considerar que las señales son una construcción "push-pull".
P : ¿Las señales introducen no determinismo en la ejecución de JavaScript?
R : No. Por un lado, todas las operaciones de Signal tienen una semántica y un orden bien definidos, y no diferirán entre las implementaciones conformes. En un nivel superior, las señales siguen un determinado conjunto de invariantes, respecto de las cuales son "sólidas". Una señal calculada siempre observa el gráfico de señal en un estado consistente y su ejecución no se ve interrumpida por otro código que muta la señal (excepto por cosas que llama a sí mismo). Vea la descripción arriba.
P : Cuando escribo en una señal de estado, ¿cuándo se programa la actualización de la señal calculada?
R : ¡No está programado! La señal calculada se volverá a calcular la próxima vez que alguien la lea. Sincrónicamente, se puede llamar a una devolución de llamada notify
de un Vigilante, lo que permite a los marcos programar una lectura en el momento que consideren apropiado.
P : ¿Cuándo entran en vigor las escrituras en señales de estado? ¿Inmediatamente o son por lotes?
R : Las escrituras en las señales de estado se reflejan inmediatamente: la próxima vez que se lea una señal calculada que depende del estado de la señal, se volverá a calcular si es necesario, incluso en la línea de código inmediatamente siguiente. Sin embargo, la pereza inherente a este mecanismo (que las señales calculadas solo se calculan cuando se leen) significa que, en la práctica, los cálculos pueden realizarse de forma por lotes.
P : ¿Qué significa que Signals permita una ejecución "sin fallos"?
R : Los modelos anteriores de reactividad basados en push enfrentaban un problema de cálculo redundante: si una actualización de una señal de estado hace que la señal calculada se ejecute con entusiasmo, en última instancia, esto puede impulsar una actualización de la interfaz de usuario. Pero esta escritura en la interfaz de usuario puede ser prematura si iba a haber otro cambio en el estado de origen de la señal antes del siguiente fotograma. A veces, incluso se mostraban valores intermedios inexactos a los usuarios finales debido a tales fallos. Las señales evitan esta dinámica al estar basadas en pull, en lugar de push: en el momento en que el marco programa la representación de la interfaz de usuario, extraerá las actualizaciones apropiadas, evitando el desperdicio de trabajo tanto en el cálculo como en la escritura en el DOM.
P : ¿Qué significa que las señales tengan "pérdidas"?
R : Esta es la otra cara de la ejecución sin fallas: las señales representan una celda de datos, solo el valor actual inmediato (que puede cambiar), no un flujo de datos a lo largo del tiempo. Por lo tanto, si escribe en una señal de estado dos veces seguidas, sin hacer nada más, la primera escritura se "pierde" y nunca será vista por ninguna señal o efecto calculado. Se entiende que esto es una característica más que un error: otras construcciones (por ejemplo, iterables asíncronos, observables) son más apropiadas para las secuencias.
P : ¿Las señales nativas serán más rápidas que las implementaciones de JS Signal existentes?
R : Eso esperamos (por un pequeño factor constante), pero esto aún debe demostrarse en el código. Los motores JS no son mágicos y, en última instancia, necesitarán implementar los mismos tipos de algoritmos que las implementaciones JS de Signals. Consulte la sección anterior sobre rendimiento.
P : ¿Por qué esta propuesta no incluye una función effect()
, cuando los efectos son necesarios para cualquier uso práctico de Signals?
R : Los efectos están inherentemente relacionados con la programación y la eliminación, que se gestionan mediante marcos y están fuera del alcance de esta propuesta. En cambio, esta propuesta incluye la base para implementar efectos a través de la API Signal.subtle.Watcher
de nivel más bajo.
P : ¿Por qué las suscripciones son automáticas en lugar de proporcionar una interfaz manual?
R : La experiencia ha demostrado que las interfaces de suscripción manual para reactividad no son ergonómicas y propensas a errores. El seguimiento automático es más componible y es una característica principal de Signals.
P : ¿Por qué la devolución de llamada de Watcher
se ejecuta de forma sincrónica, en lugar de programada en una microtarea?
R : Debido a que la devolución de llamada no puede leer ni escribir señales, no se produce ningún problema al llamarla sincrónicamente. Una devolución de llamada típica agregará una señal a una matriz para leerla más tarde o marcará un bit en alguna parte. Es innecesario y poco práctico hacer una microtarea separada para todos estos tipos de acciones.
P : A esta API le faltan algunas cosas interesantes que proporciona mi marco favorito, lo que facilita la programación con Signals. ¿Se puede agregar eso también al estándar?
R : Quizás. Todavía se están considerando varias ampliaciones. Presente un problema para generar discusión sobre cualquier característica faltante que considere importante.
P : ¿Se puede reducir el tamaño o la complejidad de esta API?
R : Definitivamente un objetivo es mantener esta API al mínimo y hemos intentado hacerlo con lo que se presenta arriba. Si tiene ideas sobre más cosas que se pueden eliminar, presente un problema para discutirlo.
P : ¿No deberíamos comenzar el trabajo de estandarización en esta área con un concepto más primitivo, como los observables?
R : Los observables pueden ser una buena idea para algunas cosas, pero no resuelven los problemas que las señales pretenden resolver. Como se describió anteriormente, los observables u otros mecanismos de publicación/suscripción no son una solución completa para muchos tipos de programación de UI, debido a demasiado trabajo de configuración propenso a errores para los desarrolladores y trabajo desperdiciado debido a la falta de pereza, entre otros problemas.
P : ¿Por qué se proponen Signals en TC39 en lugar de DOM, dado que la mayoría de sus aplicaciones están basadas en web?
R : Algunos coautores de esta propuesta están interesados en entornos de UI no web como objetivo, pero hoy en día, cualquiera de los dos lugares puede ser adecuado para eso, ya que las API web se implementan con mayor frecuencia fuera de la web. En última instancia, Signals no necesita depender de ninguna API DOM, por lo que cualquier forma funciona. Si alguien tiene una buena razón para cambiar este grupo, háganoslo saber en un problema. Por ahora, todos los contribuyentes han firmado los acuerdos de propiedad intelectual del TC39 y el plan es presentarlos al TC39.
P : ¿Cuánto tiempo pasará hasta que pueda usar Signals estándar?
R : Ya hay un polyfill disponible, pero es mejor no confiar en su estabilidad, ya que esta API evoluciona durante su proceso de revisión. En algunos meses o un año, debería poder utilizarse un polyfill estable de alta calidad y alto rendimiento, pero esto aún estará sujeto a revisiones del comité y aún no será estándar. Siguiendo la trayectoria típica de una propuesta TC39, se espera que pasen al menos 2 o 3 años como mínimo absoluto para que Signals esté disponible de forma nativa en todos los navegadores desde algunas versiones anteriores, de modo que no se necesiten polyfills.
P : ¿Cómo evitaremos estandarizar el tipo incorrecto de señales demasiado pronto, como {{JS/función web que no te gusta}}?
R : Los autores de esta propuesta planean hacer un esfuerzo adicional con la creación de prototipos y pruebas antes de solicitar un avance de etapa en el TC39. Consulte "Estado y plan de desarrollo" más arriba. Si ve lagunas en este plan u oportunidades de mejora, presente un problema explicandolo.