Rascunho da proposta de padronização de JavaScript Signals
- Documento que descreve uma direção comum inicial para sinais (signals) no JavaScript, semelhante ao esforço Promises/A+ antes da padronização de Promises pelo TC39 no ES2015.
- Esse esforço está focado em coordenar o ecossistema JavaScript e, se essa coordenação for bem-sucedida, um padrão poderá surgir com base nessa experiência.
- Autores de vários frameworks estão colaborando em um modelo comum que possa sustentar núcleos de reatividade.
- O rascunho atual se baseia em contribuições de design de autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz e outros.
Contexto: por que signals?
- Para desenvolver interfaces de usuário (UI) complexas, desenvolvedores de aplicações JavaScript precisam armazenar, calcular, invalidar, sincronizar e propagar estado de forma eficiente para a camada de visualização da aplicação.
- A UI frequentemente envolve não apenas gerenciar valores simples, mas também renderizar estado computado que depende de outros valores ou estados.
- O objetivo dos signals é fornecer a infraestrutura para gerenciar esse estado da aplicação, permitindo que desenvolvedores foquem na lógica de negócio em vez de em detalhes repetitivos.
Exemplo - contador em VanillaJS
- Há uma variável chamada
counter, e queremos atualizar no DOM se o contador é par sempre que esse valor mudar.
- Em Vanilla JS, o código poderia ser assim:
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);
- Esse código tem alguns problemas:
- Atualizar
counter gera muito ruído e boilerplate.
- O estado de
counter fica fortemente acoplado ao sistema de renderização.
- Se
counter mudar mas parity não mudar (por exemplo, de 2 para 4), ainda assim haverá cálculo e renderização desnecessários.
- E se outra parte da UI quiser renderizar apenas quando
counter for atualizado?
- Outras partes da UI que dependem apenas de
isEven ou parity não conseguem ser atualizadas sem interagir diretamente com counter.
Introdução aos signals
- A abstração de data binding entre modelo e visualização é central em frameworks de UI há muito tempo, mesmo sem haver um mecanismo desse tipo embutido em JS ou na plataforma web.
- Dentro de frameworks e bibliotecas JS, houve muita experimentação com diferentes formas de representar esse binding, e o poder da abordagem de valores reativos de primeira classe — frequentemente chamada de "Signals" — para representar estado ou cálculos derivados de outros dados foi comprovado.
- Reimaginando o exemplo acima com uma API de signals, temos o seguinte:
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);
Motivação para padronizar signals
Interoperabilidade
- Cada implementação de signals tem seu próprio mecanismo de rastreamento automático, o que dificulta compartilhar modelos, componentes e bibliotecas entre diferentes frameworks.
- O objetivo desta proposta é separar completamente o modelo reativo da view de renderização, para que desenvolvedores não precisem reescrever código não relacionado à UI ao migrar para uma nova tecnologia de renderização, ou possam desenvolver em JS modelos reativos compartilháveis que possam ser implantados em outros contextos.
Desempenho/uso de memória
- Como bibliotecas comumente usadas já viriam embutidas, enviar menos código sempre pode trazer um pequeno ganho potencial de desempenho, mas como implementações de signals geralmente são bem pequenas, não se espera que esse efeito seja muito grande.
Ferramentas de desenvolvimento
- Ao usar bibliotecas de signals existentes em JS, é difícil rastrear call stacks através da cadeia de signals computados, o grafo de referências entre signals etc.
- Signals embutidos permitiriam que o runtime JS e as ferramentas de desenvolvimento oferecessem melhor suporte para inspecionar signals.
Benefícios adicionais
Benefícios da biblioteca padrão
- Em geral, JavaScript sempre teve uma biblioteca padrão relativamente mínima, mas a tendência no TC39 é transformar JS em uma linguagem "com pilhas incluídas" (batteries included), com um conjunto de funcionalidades nativas de alta qualidade.
Integração com HTML/DOM (possibilidade futura)
- O W3C e implementadores de navegadores estão atualmente trabalhando para introduzir templates nativos em HTML.
- Para atingir esses objetivos, eventualmente serão necessários valores primitivos reativos no HTML.
Objetivos de design dos signals
- As bibliotecas de signals existentes não diferem tanto assim em seu núcleo.
- Esta proposta busca se apoiar no sucesso delas, implementando características importantes de muitas dessas bibliotecas.
Funcionalidades principais
- Um tipo Signal que representa estado, ou seja, um Signal gravável.
- Um tipo Signal computado/memo/derivado que depende de outros signals, é calculado de forma preguiçosa e fica em cache.
- Permitir que frameworks JS façam seu próprio agendamento.
Esboço da API
- A ideia inicial para a API de signals é a seguinte. Este é apenas um rascunho inicial e deve mudar com o tempo.
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 dos signals
- Descreve os algoritmos implementados para cada API exposta ao JavaScript.
- Isso pode ser considerado uma especificação inicial, com o objetivo de definir o máximo possível de semântica diante de mudanças ainda bastante abertas.
Opinião do GN⁺
- A proposta de padronização de JavaScript Signals tem como objetivo melhorar a interoperabilidade entre frameworks e facilitar a implementação de programação reativa por desenvolvedores.
- A proposta é uma tentativa de padronizar as funcionalidades centrais de várias bibliotecas de signals existentes, podendo oferecer aos desenvolvedores um modelo de programação consistente.
- O conceito de signals pode ser útil não apenas no desenvolvimento de UI, mas também em contextos fora de UI, especialmente em sistemas de build, ajudando a evitar rebuilds desnecessários.
- A API proposta oferece ferramentas úteis para desenvolvedores de frameworks, com potencial para alcançar melhor desempenho e gerenciamento de memória.
- No entanto, para que essa tecnologia seja amplamente adotada, ainda serão necessários mais protótipos e feedback da comunidade, além de comprovação de sua eficácia quando integrada a aplicações reais.
- Atualmente, frameworks como React, Vue e Svelte já possuem seus próprios sistemas reativos, e a compatibilidade ou estratégia de integração com esses frameworks também será uma consideração importante.
1 comentários
Comentários do Hacker News
Exemplo de Vanilla JS vs. Signals
isEvenouparity, talvez seja preciso mudar toda a abordagem.Promises e as mudanças no JavaScript
new Promisecom frequência, mas na prática quase não usei..then, o que simplifica a interface com várias bibliotecas de terceiros.Signals como parte da linguagem
Uso de eventos na aplicação
window.dispatchEventewindow.addEventListener.Dificuldade de gerenciar estado do DOM e atualizações
Promises e programação assíncrona
S.js e Signals
Signals semelhantes ao MobX
Adicionar frameworks à biblioteca padrão
Entendimento e problemas da proposta de Signal
effectdetecta mudanças emparitye se essa lambda é chamada para qualquer mudança de sinal.