Borrador de propuesta del estándar JavaScript Signals
- Documento que describe una dirección común inicial para las señales (
signals) en JavaScript, similar al esfuerzo de Promises/A+ previo a que TC39 estandarizara Promises en ES2015.
- Este esfuerzo se centra en coordinar el ecosistema de JavaScript, y si esa coordinación tiene éxito, a partir de esa experiencia podría surgir un estándar.
- Varios autores de frameworks están colaborando en un modelo común que pueda respaldar núcleos de reactividad.
- El borrador actual se basa en aportes de diseño de autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue y Wiz.
Contexto: ¿por qué signals?
- Para desarrollar interfaces de usuario (UI) complejas, los desarrolladores de aplicaciones JavaScript necesitan almacenar, calcular, invalidar, sincronizar y enviar el estado de la aplicación a la capa de vista de forma eficiente.
- La UI no solo implica administrar valores simples, sino también renderizar estado calculado que depende de otros valores o estados.
- El objetivo de las signals es proporcionar la infraestructura para administrar este estado de aplicación, de modo que los desarrolladores puedan enfocarse en la lógica de negocio en lugar de en detalles repetitivos.
Ejemplo - contador en VanillaJS
- Hay una variable llamada
counter, y se quiere actualizar en el DOM si el contador es par o impar cada vez que cambie.
- En Vanilla JS podría haber código como el siguiente:
let counter = 0;
const setCounter = (value) => {
counter = value;
render();
};
const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();
// Simulate external updates to counter...
setInterval(() => setCounter(counter + 1), 1000);
- Este código tiene varios problemas:
- Asignar
counter genera mucho ruido y bastante boilerplate.
- El estado de
counter está fuertemente acoplado al sistema de renderizado.
- Si
counter cambia pero parity no cambia (por ejemplo, de 2 a 4), se hacen cálculos y renderizados innecesarios.
- Puede haber otras partes de la UI que solo quieran renderizarse cuando se actualice
counter.
- Otras partes de la UI que dependan solo de
isEven o parity no pueden actualizarse sin interactuar directamente con counter.
Introducción a signals
- La abstracción de data binding entre modelo y vista ha sido durante mucho tiempo un núcleo de los frameworks de UI, aunque JS o la plataforma web no tengan un mecanismo así incorporado.
- Dentro de frameworks y librerías de JS se ha experimentado mucho con distintas formas de representar estos enlaces, y se ha demostrado la potencia del enfoque de valores reactivos de primera clase que representan cálculos derivados del estado u otros datos, a menudo llamados "Signals".
- Si reimaginamos el ejemplo anterior usando la API de signals, quedaría así:
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");
// A library or framework defines effects based on other Signal primitives
declare function effect(cb: () => void): (() => void);
effect(() => element.innerText = parity.get());
// Simulate external updates to counter...
setInterval(() => counter.set(counter.get() + 1), 1000);
Motivación para estandarizar signals
Interoperabilidad
- Cada implementación de signals tiene su propio mecanismo de seguimiento automático, lo que dificulta compartir modelos, componentes y librerías entre distintos frameworks.
- El objetivo de esta propuesta es separar por completo el modelo reactivo de la vista de renderizado, para que los desarrolladores no tengan que reescribir código no relacionado con UI al migrar a una nueva tecnología de renderizado, o para que puedan desarrollar en JS modelos reactivos compartidos que se desplieguen en otros contextos.
Rendimiento/uso de memoria
- Como una librería de uso común estaría incorporada, enviar menos código siempre puede traer una pequeña mejora potencial de rendimiento, aunque no se espera que el efecto sea muy grande porque las implementaciones de signals suelen ser bastante pequeñas.
Herramientas para desarrolladores
- Al usar librerías de signals existentes en JS, es difícil rastrear el call stack a través de cadenas de signals computadas, los grafos de referencia entre signals, etc.
- Las signals integradas permitirían que el runtime de JS y las herramientas de desarrollo ofrezcan mejor soporte para inspeccionarlas.
Beneficios secundarios
Beneficios de la biblioteca estándar
- En general, JavaScript ha tenido una biblioteca estándar bastante minimalista, pero la tendencia de TC39 es convertir a JS en un lenguaje "con baterías incluidas", con un conjunto de funcionalidades integradas de alta calidad.
Integración con HTML/DOM (posibilidad futura)
- El W3C y los implementadores de navegadores están trabajando actualmente en introducir plantillas nativas en HTML.
- Para lograr esos objetivos, eventualmente harán falta primitivas reactivas en HTML.
Objetivos de diseño de signals
- Las librerías de signals existentes no son tan distintas en su núcleo.
- Esta propuesta busca construir sobre su éxito implementando características importantes de muchas librerías.
Funcionalidades principales
- Un tipo Signal que represente estado, es decir, una Signal escribible.
- Un tipo Signal computada/memo/derivada que dependa de otras signals, se calcule de forma perezosa y quede en caché.
- Permitir que los frameworks de JS hagan su propia planificación (
scheduling).
Boceto de API
- La idea inicial de la API de signals es la siguiente. Esto es solo un borrador inicial y se espera que cambie con el tiempo.
namespace Signal {
// A read-write Signal
class State<T> implements Signal<T> {
// Create a state Signal starting with the value t
constructor(t: T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
// Set the state Signal value to t
set(t: T): void;
}
// A Signal which is a formula based on other Signals
class Computed<T> implements Signal<T> {
// Create a Signal which evaluates to the value returned by the callback.
// Callback is called with this signal as the this value.
constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
}
// This namespace includes "advanced" features that are better to
// leave for framework authors rather than application developers.
// Analogous to `crypto.subtle`
namespace subtle {
// Run a callback with all tracking disabled (even for nested computed).
function untrack<T>(cb: () => T): T;
// Get the current computed signal which is tracking any signal reads, if any
function currentComputed(): Computed | null;
// Returns ordered list of all signals which this one referenced
// during the last time it was evaluated.
// For a Watcher, lists the set of signals which it is watching.
function introspectSources(s: Computed | Watcher): (State | Computed)[];
// Returns the Watchers that this signal is contained in, plus any
// Computed signals which read this signal last time they were evaluated,
// if that computed signal is (recursively) watched.
function introspectSinks(s: State | Computed): (Computed | Watcher)[];
// True if this signal is "live", in that it is watched by a Watcher,
// or it is read by a Computed signal which is (recursively) live.
function hasSinks(s: State | Computed): boolean;
// True if this element is "reactive", in that it depends
// on some other signal. A Computed where hasSources is false
// will always return the same constant.
function hasSources(s: Computed | Watcher): boolean;
class Watcher {
// When a (recursive) source of Watcher is written to, call this callback,
// if it hasn't already been called since the last `watch` call.
// No signals may be read or written during the notify.
constructor(notify: (this: Watcher) => void);
// Add these signals to the Watcher's set, and set the watcher to run its
// notify callback next time any signal in the set (or one of its dependencies) changes.
// Can be called with no arguments just to reset the "notified" state, so that
// the notify callback will be invoked again.
watch(...s: Signal[]): void;
// Remove these signals from the watched set (e.g., for an effect which is disposed)
unwatch(...s: Signal[]): void;
// Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
// with a source which is dirty or pending and hasn't yet been re-evaluated
getPending(): Signal[];
}
// Hooks to observe being watched or no longer watched
var watched: Symbol;
var unwatched: Symbol;
}
interface Options<T> {
// Custom comparison function between old and new value. Default: Object.is.
// The signal is passed in as the this value for context.
equals?: (this: Signal<T>, t: T, t2: T) => boolean;
// Callback called when isWatched becomes true, if it was previously false
[Signal.subtle.watched]?: (this: Signal<T>) => void;
// Callback called whenever isWatched becomes false, if it was previously true
[Signal.subtle.unwatched]?: (this: Signal<T>) => void;
}
}
Algoritmo de signals
- Describe los algoritmos que implementan cada API expuesta a JavaScript.
- Puede verse como una especificación inicial, y busca fijar lo más posible un conjunto de semánticas abiertas a cambios importantes.
Opinión de GN⁺
- La propuesta del estándar JavaScript Signals busca mejorar la interoperabilidad entre frameworks y facilitar que los desarrolladores implementen programación reactiva.
- Esta propuesta es un intento de estandarizar funcionalidades centrales de varias librerías de signals existentes, y podría ofrecer a los desarrolladores un modelo de programación consistente.
- El concepto de signals puede ser útil no solo en el desarrollo de UI, sino también en contextos no relacionados con UI, especialmente para evitar recompilaciones innecesarias en sistemas de build.
- La API propuesta ofrece herramientas útiles para desarrolladores de frameworks, con las que se espera lograr mejor rendimiento y administración de memoria.
- Sin embargo, para que esta tecnología sea adoptada ampliamente, hace falta más prototipado y retroalimentación de la comunidad, además de probar su efectividad al integrarla en aplicaciones reales.
- Actualmente, frameworks como React, Vue y Svelte ya tienen sus propios sistemas de reactividad, por lo que la compatibilidad o estrategia de integración con estos frameworks también será una consideración importante.
1 comentarios
Opiniones de Hacker News
Ejemplo de Vanilla JS vs. Signals
isEvenoparity, puede que sea necesario cambiar todo el enfoque.Promises y los cambios en JavaScript
new Promisecon frecuencia, pero en la práctica casi nunca lo usé..then, y eso simplificó la interfaz con varias bibliotecas de terceros.Signals como parte del lenguaje
Uso de eventos en la aplicación
window.dispatchEventywindow.addEventListener.Dificultad del manejo de estado del DOM y las actualizaciones
Promises y programación asíncrona
S.js y Signals
Signals similares a MobX
Agregar frameworks a la biblioteca estándar
Comprensión y problemas de la propuesta de Signal
effectdetecta cambios en parity y si llama a esta lambda ante cualquier cambio de señal.