Etapa 1 (explicação)
Campeões da proposta TC39: Daniel Ehrenberg, Yehuda Katz, Jatin Ramanathan, Shay Lewis, Kristen Hewell Garrett, Dominic Gannaway, Preston Sego, Milo M, Rob Eisenberg
Autores originais: Rob Eisenberg e Daniel Ehrenberg
Este documento descreve uma direção comum inicial para sinais em JavaScript, semelhante ao esforço Promises/A+ que precedeu as Promessas padronizadas pelo TC39 no ES2015. Experimente você mesmo, usando um polyfill.
Da mesma forma que Promises/A+, este esforço se concentra no alinhamento do ecossistema JavaScript. Se este alinhamento for bem-sucedido, então poderá surgir um padrão, baseado nessa experiência. Vários autores de frameworks estão colaborando aqui em um modelo comum que poderia apoiar seu núcleo de reatividade. O rascunho atual é baseado nas contribuições de design dos autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz e muito mais…
Diferentemente do Promises/A+, não estamos tentando resolver uma API de superfície comum voltada para o desenvolvedor, mas sim a semântica central precisa do gráfico de sinal subjacente. Esta proposta inclui uma API totalmente concreta, mas a API não é direcionada à maioria dos desenvolvedores de aplicativos. Em vez disso, a API de sinal aqui é mais adequada para a construção de estruturas, fornecendo interoperabilidade por meio de gráfico de sinal comum e mecanismo de rastreamento automático.
O plano para esta proposta é fazer uma prototipagem inicial significativa, incluindo a integração em vários frameworks, antes de avançar além do Estágio 1. Só estamos interessados em padronizar Sinais se eles forem adequados para uso na prática em múltiplos frameworks e fornecerem benefícios reais em relação aos frameworks. forneceu sinais. Esperamos que a prototipagem inicial significativa nos forneça essas informações. Consulte "Status e plano de desenvolvimento" abaixo para obter mais detalhes.
Para desenvolver uma interface de usuário (IU) complicada, os desenvolvedores de aplicativos JavaScript precisam armazenar, calcular, invalidar, sincronizar e enviar o estado para a camada de visualização do aplicativo de maneira eficiente. As UIs geralmente envolvem mais do que apenas gerenciar valores simples, mas geralmente envolvem a renderização de um estado computado que depende de uma árvore complexa de outros valores ou estado que também é computado. O objetivo do Signals é fornecer infraestrutura para gerenciar esse estado de aplicativo, para que os desenvolvedores possam se concentrar na lógica de negócios, em vez de nesses detalhes repetitivos.
Construções semelhantes a sinais também foram consideradas úteis em contextos não UI, particularmente em sistemas de construção para evitar reconstruções desnecessárias.
Os sinais são usados na programação reativa para eliminar a necessidade de gerenciar atualizações em aplicativos.
Um modelo de programação declarativa para atualização com base em mudanças de estado.
de O que é reatividade? .
Dada uma variável counter
, você deseja renderizar no DOM se o contador é par ou ímpar. Sempre que o counter
muda, você deseja atualizar o DOM com a paridade mais recente. No Vanilla JS, você pode ter algo assim:
deixe contador = 0;const setCounter = (valor) => { contador = valor; render();};const isEven = () => (contador & 1) == 0;const paridade = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// Simula atualizações externas para counter...setInterval(() => setCounter(counter + 1), 1000);
Isso tem vários problemas...
A configuração counter
é barulhenta e pesada.
O estado counter
está fortemente acoplado ao sistema de renderização.
Se o counter
mudar, mas parity
não (por exemplo, o contador vai de 2 para 4), então fazemos cálculos desnecessários da paridade e renderização desnecessária.
E se outra parte de nossa IU quiser apenas ser renderizada quando o counter
for atualizado?
E se outra parte de nossa IU depender apenas de isEven
ou apenas parity
?
Mesmo neste cenário relativamente simples, vários problemas surgem rapidamente. Poderíamos tentar contornar isso introduzindo pub/sub para o counter
. Isso permitiria que consumidores adicionais do counter
pudessem se inscrever para adicionar suas próprias reações às mudanças de estado.
No entanto, ainda estamos presos aos seguintes problemas:
A função render, que depende apenas da parity
, deve "saber" que realmente precisa se inscrever em counter
.
Não é possível atualizar a UI com base apenas em isEven
ou parity
, sem interagir diretamente com counter
.
Aumentamos nosso padrão. Sempre que você estiver usando algo, não é apenas uma questão de chamar uma função ou ler uma variável, mas sim se inscrever e fazer atualizações lá. Gerenciar o cancelamento de assinatura também é especialmente complicado.
Agora, poderíamos resolver alguns problemas adicionando pub/sub não apenas a counter
, mas também a isEven
e parity
. Teríamos então que assinar isEven
para counter
, parity
para isEven
e render
para parity
. Infelizmente, não apenas nosso código clichê explodiu, mas também estamos presos a uma tonelada de contabilidade de assinaturas e a um possível desastre de vazamento de memória se não limparmos tudo da maneira certa. Então, resolvemos alguns problemas, mas criamos uma nova categoria de problemas e muito código. Para piorar a situação, temos que passar por todo esse processo para cada parte do estado do nosso sistema.
As abstrações de vinculação de dados em UIs para o modelo e a visualização têm sido fundamentais para estruturas de UI em várias linguagens de programação, apesar da ausência de qualquer mecanismo desse tipo integrado ao JS ou à plataforma web. Dentro das estruturas e bibliotecas JS, tem havido uma grande quantidade de experimentação em diferentes maneiras de representar essa ligação, e a experiência tem mostrado o poder do fluxo de dados unidirecional em conjunto com um tipo de dados de primeira classe que representa uma célula de estado ou computação. derivado de outros dados, agora frequentemente chamados de "Sinais". Essa abordagem de valor reativo de primeira classe parece ter feito sua primeira aparição popular em estruturas web JavaScript de código aberto com o Knockout em 2010. Nos anos seguintes, muitas variações e implementações foram criadas. Nos últimos 3-4 anos, as abordagens primitivas e relacionadas do Signal ganharam ainda mais força, com quase todas as bibliotecas ou estruturas JavaScript modernas tendo algo semelhante, sob um nome ou outro.
Para entender os Sinais, vamos dar uma olhada no exemplo acima, reimaginado com uma API Signal articulada mais detalhadamente abaixo.
const contador = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven .get() ? "even" : "odd");// Uma biblioteca ou estrutura define efeitos com base em outras primitivas de sinaldeclare function effect(cb: () => void): (() => void);effect(( ) => element.innerText = parity.get());// Simula atualizações externas para counter...setInterval(() => counter.set(counter.get() + 1), 1000);
Existem algumas coisas que podemos ver imediatamente:
Eliminamos o ruído padrão em torno da variável counter
do nosso exemplo anterior.
Existe uma API unificada para lidar com valores, cálculos e efeitos colaterais.
Não há problema de referência circular ou dependências invertidas entre counter
e render
.
Não há assinaturas manuais, nem há necessidade de escrituração contábil.
Existe um meio de controlar o tempo/programação dos efeitos colaterais.
Os sinais nos dão muito mais do que pode ser visto na superfície da API:
Rastreamento Automático de Dependência - Um Sinal computado descobre automaticamente quaisquer outros Sinais dos quais depende, sejam esses Sinais valores simples ou outros cálculos.
Avaliação preguiçosa - Os cálculos não são avaliados avidamente quando são declarados, nem são avaliados imediatamente quando suas dependências mudam. Eles só são avaliados quando seu valor é solicitado explicitamente.
Memoização - Os Sinais Computados armazenam em cache seu último valor para que os cálculos que não possuem alterações em suas dependências não precisem ser reavaliados, não importa quantas vezes sejam acessados.
Cada implementação do Signal possui seu próprio mecanismo de rastreamento automático, para rastrear as fontes encontradas ao avaliar um sinal computado. Isso dificulta o compartilhamento de modelos, componentes e bibliotecas entre diferentes estruturas – eles tendem a vir com um falso acoplamento ao seu mecanismo de visualização (dado que os Sinais geralmente são implementados como parte de estruturas JS).
Um objetivo desta proposta é dissociar totalmente o modelo reativo da visão de renderização, permitindo que os desenvolvedores migrem para novas tecnologias de renderização sem reescrever seu código não-UI, ou desenvolver modelos reativos compartilhados em JS para serem implantados em diferentes contextos. Infelizmente, devido ao controle de versão e à duplicação, revelou-se impraticável atingir um nível forte de compartilhamento por meio de bibliotecas no nível JS – os integrados oferecem uma garantia de compartilhamento mais forte.
É sempre um pequeno aumento potencial de desempenho enviar menos código devido às bibliotecas comumente usadas serem integradas, mas as implementações de Sinais são geralmente muito pequenas, então não esperamos que esse efeito seja muito grande.
Suspeitamos que as implementações nativas em C++ de estruturas de dados e algoritmos relacionados ao Signal podem ser um pouco mais eficientes do que o que é possível em JS, por um fator constante. No entanto, nenhuma mudança algorítmica é prevista em relação ao que estaria presente em um polyfill; não se espera que os motores sejam mágicos aqui, e os próprios algoritmos de reatividade serão bem definidos e inequívocos.
O grupo campeão espera desenvolver diversas implementações de Sinais e usá-las para investigar essas possibilidades de desempenho.
Com as bibliotecas Signal existentes na linguagem JS, pode ser difícil rastrear coisas como:
A pilha de chamadas em uma cadeia de sinais computados, mostrando a cadeia causal de um erro
O gráfico de referência entre Sinais, quando um depende do outro – importante ao depurar o uso de memória
Sinais integrados permitem que tempos de execução JS e DevTools tenham suporte potencialmente aprimorado para inspeção de sinais, especialmente para depuração ou análise de desempenho, seja integrado em navegadores ou por meio de uma extensão compartilhada. Ferramentas existentes, como o inspetor de elementos, o instantâneo de desempenho e os criadores de perfil de memória, podem ser atualizadas para destacar especificamente os sinais em sua apresentação de informações.
Em geral, o JavaScript tem uma biblioteca padrão mínima, mas uma tendência no TC39 tem sido tornar o JS uma linguagem mais "incluída com baterias", com um conjunto integrado de funcionalidades de alta qualidade disponível. Por exemplo, Temporal está substituindo moment.js, e uma série de pequenos recursos, por exemplo, Array.prototype.flat
e Object.groupBy
estão substituindo muitos casos de uso de lodash. Os benefícios incluem pacotes menores, maior estabilidade e qualidade, menos aprendizado ao ingressar em um novo projeto e um vocabulário geralmente comum entre os desenvolvedores JS.
O trabalho atual no W3C e por implementadores de navegadores está buscando trazer modelos nativos para HTML (DOM Parts and Template Instantiation). Além disso, o W3C Web Components CG está explorando a possibilidade de estender os Web Components para oferecer uma API HTML totalmente declarativa. Para atingir esses dois objetivos, eventualmente uma primitiva reativa será necessária para o HTML. Além disso, muitas melhorias ergonômicas no DOM através da integração de Sinais podem ser imaginadas e solicitadas pela comunidade.
Observe que essa integração seria um esforço separado que ocorreria posteriormente, e não faria parte desta proposta em si.
Os esforços de padronização às vezes podem ser úteis apenas no nível da “comunidade”, mesmo sem alterações nos navegadores. O esforço da Signals está reunindo muitos autores de frameworks diferentes para uma discussão profunda sobre a natureza da reatividade, algoritmos e interoperabilidade. Isto já foi útil e não justifica a inclusão em motores e navegadores JS; Os sinais só devem ser adicionados ao padrão JavaScript se houver benefícios significativos além da troca de informações do ecossistema possibilitada.
Acontece que as bibliotecas Signal existentes não são tão diferentes umas das outras, em sua essência. Esta proposta visa aproveitar o seu sucesso através da implementação das qualidades importantes de muitas dessas bibliotecas.
Um tipo de sinal que representa o estado, ou seja, sinal gravável. Este é um valor que outros podem ler.
Um tipo de sinal calculado/memorando/derivado, que depende de outros e é calculado e armazenado em cache preguiçosamente.
A computação é lenta, o que significa que os sinais computados não são calculados novamente por padrão quando uma de suas dependências é alterada, mas apenas executados se alguém realmente os ler.
A computação é "livre de falhas", o que significa que nenhum cálculo desnecessário é realizado. Isto implica que, quando uma aplicação lê um sinal computado, há uma classificação topológica das partes potencialmente sujas do gráfico a serem executadas, para eliminar quaisquer duplicatas.
A computação é armazenada em cache, o que significa que se, após a última alteração de uma dependência, nenhuma dependência tiver sido alterada, o sinal calculado não será recalculado quando acessado.
Comparações personalizadas são possíveis para sinais computados, bem como para sinais de estado, para observar quando outros sinais computados que dependem deles devem ser atualizados.
As reações à condição em que um sinal computado tem uma de suas dependências (ou dependências aninhadas) tornam-se "sujas" e mudam, o que significa que o valor do sinal pode estar desatualizado.
Essa reação tem como objetivo agendar um trabalho mais significativo a ser executado posteriormente.
Os efeitos são implementados em termos dessas reações, além do agendamento no nível da estrutura.
Os sinais computados precisam da capacidade de reagir se são registrados como uma dependência (aninhada) de uma dessas reações.
Permita que estruturas JS façam seu próprio agendamento. Sem agendamento forçado integrado no estilo Promise.
São necessárias reações síncronas para permitir o agendamento de trabalhos posteriores com base na lógica da estrutura.
As gravações são síncronas e entram em vigor imediatamente (uma estrutura na qual as gravações em lote podem fazer isso).
É possível separar a verificação se um efeito pode estar "sujo" da execução real do efeito (habilitando um agendador de efeitos de dois estágios).
Capacidade de ler sinais sem acionar dependências a serem registradas ( untrack
)
Habilite a composição de diferentes bases de código que usam sinais/reatividade, por exemplo,
Usando várias estruturas juntas no que diz respeito ao rastreamento/reatividade em si (omissões de módulo, veja abaixo)
Estruturas de dados reativos independentes de estrutura (por exemplo, proxy de armazenamento recursivamente reativo, mapa e conjunto e matriz reativos, etc.)
Desencorajar/proibir o uso indevido ingênuo de reações síncronas.
Risco de solidez: pode expor "falhas" se usado incorretamente: Se a renderização for feita imediatamente quando um sinal for definido, poderá expor o estado incompleto do aplicativo ao usuário final. Portanto, esse recurso só deve ser usado para agendar de forma inteligente o trabalho para mais tarde, quando a lógica da aplicação for concluída.
Solução: proibir a leitura e gravação de qualquer sinal em um retorno de chamada de reação síncrona
Desencoraje untrack
e marque sua natureza doentia
Risco de solidez: permite a criação de Sinais computados cujo valor depende de outros Sinais, mas que não são atualizados quando esses Sinais mudam. Deve ser utilizado quando os acessos não rastreados não alterarão o resultado do cálculo.
Solução: a API está marcada como "insegura" no nome.
Nota: Esta proposta permite que os sinais sejam lidos e escritos a partir de sinais computados e de efeito, sem restringir as gravações que vêm após as leituras, apesar do risco de integridade. Esta decisão foi tomada para preservar a flexibilidade e compatibilidade na integração com frameworks.
Deve ser uma base sólida para que vários frameworks implementem seus mecanismos de sinais/reatividade.
Deve ser uma boa base para proxies de armazenamento recursivos, reatividade de campo de classe baseada em decorador e APIs de estilo .value
e [state, setState]
.
A semântica é capaz de expressar os padrões válidos possibilitados por diferentes frameworks. Por exemplo, deveria ser possível que esses sinais fossem a base de escritas imediatamente refletidas ou de escritas que são agrupadas e aplicadas posteriormente.
Seria bom se esta API pudesse ser usada diretamente por desenvolvedores de JavaScript.
Ideia: Forneça todos os ganchos, mas inclua erros quando mal utilizados, se possível.
Idéia: colocar APIs sutis em um namespace subtle
, semelhante a crypto.subtle
, para marcar a linha entre APIs que são necessárias para uso mais avançado, como implementar uma estrutura ou construir ferramentas de desenvolvimento, versus uso mais diário de desenvolvimento de aplicativos, como instanciar sinais para uso com um estrutura.
No entanto, é importante não ocultar literalmente exatamente os mesmos nomes!
Se um recurso corresponde a um conceito de ecossistema, é bom usar um vocabulário comum.
Tensão entre "usabilidade por desenvolvedores JS" e "fornecer todos os ganchos para frameworks"
Ser implementável e utilizável com bom desempenho – a API de superfície não causa muita sobrecarga
Habilite a subclasse, para que as estruturas possam adicionar seus próprios métodos e campos, incluindo campos privados. Isto é importante para evitar a necessidade de dotações adicionais a nível do quadro. Consulte "Gerenciamento de memória" abaixo.
Se possível: Um sinal computado deve ser coletado como lixo se nada ativo estiver fazendo referência a ele para possíveis leituras futuras, mesmo que esteja vinculado a um gráfico mais amplo que permaneça ativo (por exemplo, lendo um estado que permanece ativo).
Observe que a maioria das estruturas hoje exige o descarte explícito de sinais computados se eles tiverem alguma referência de ou para outro gráfico de sinal que permaneça ativo.
Isso acaba não sendo tão ruim quando sua vida útil está vinculada à vida útil de um componente de UI e os efeitos precisam ser descartados de qualquer maneira.
Se for muito caro executar com essa semântica, então deveríamos adicionar o descarte explícito (ou "desvinculação") de Sinais computados à API abaixo, que atualmente não possui isso.
Uma meta relacionada separada: Minimizar o número de alocações, por exemplo,
para criar um sinal gravável (evite dois encerramentos + array separados)
para implementar efeitos (evitar um fechamento para cada reação)
Na API para observar alterações do Signal, evite criar estruturas de dados temporárias adicionais
Solução: API baseada em classes permitindo a reutilização de métodos e campos definidos em subclasses
Uma ideia inicial de uma API Signal está abaixo. Observe que este é apenas um rascunho inicial e prevemos mudanças ao longo do tempo. Vamos começar com o .d.ts
completo para ter uma ideia do formato geral e depois discutiremos os detalhes do que tudo isso significa.
interface Signal<T> {// Obtém o valor do signalget(): T;}namespace Signal {// Uma classe de sinal de leitura e gravação State<T> implementa Signal<T> {// Cria um estado Signal começando com o valor tconstructor(t: T, options?: SignalOptions<T>);// Obtém o valor do signalget(): T;// Define o valor do sinal do estado como tset(t: T): void;}// Um sinal que é uma fórmula baseada em outra classe de sinais Computed<T = desconhecido> implementa Signal<T> {// Cria um Signal que avalia o valor retornado pelo retorno de chamada. // O retorno de chamada é chamado com este sinal como this value.constructor(cb: (this: Computed<T >) => T, options?: SignalOptions<T>);// Obtém o valor do signalget(): T;}// Este namespace inclui recursos "avançados" que são melhores // deixados para os autores do framework em vez de aplicativo desenvolvedores.// Análogo a `crypto.subtle`namespace sutil {// Executa um retorno de chamada com todo o rastreamento desabilitadofunction untrack<T>(cb: () => T): T;// Obtém o sinal computado atual que está rastreando qualquer sinal lê, se houver função currentComputed(): Computed | null;// Retorna uma lista ordenada de todos os sinais aos quais este referenciou // durante a última vez que foi avaliado. // Para um Watcher, lista o conjunto de sinais que ele está observando.function introspectSources(s: Computed | Watcher): (State | Computed)[];// Retorna os Watchers que este sinal está contido, mais quaisquer // Sinais computados que leram este sinal na última vez em que foram avaliados,// se esse sinal computado for (recursivamente) watch.function introspectSinks(s: State | Computed): (Computed | Watcher)[];// Verdadeiro se este sinal estiver "ao vivo", na medida em que é observado por um Watcher, // ou é lido por um sinal Computed que é (recursivamente) live.function hasSinks(s: State | Computed): boolean;// True se este elemento for "reativo", pois depende // de algum outro sinal. Um Computed onde hasSources é false// sempre retornará a mesma constante.function hasSources(s: Computed | Watcher): boolean;class Watcher {// Quando uma fonte (recursiva) do Watcher é gravada, chame este retorno de chamada,// se ainda não tiver sido chamado desde a última chamada `watch`.// Nenhum sinal pode ser lido ou escrito durante o notify.constructor(notify: (this: Watcher) => void);// Adicione estes sinais para o conjunto do Observador e configure o observador para executar seu // notificar retorno de chamada na próxima vez que qualquer sinal no conjunto (ou uma de suas dependências) for alterado. // Pode ser chamado sem argumentos apenas para redefinir o estado "notificado", então que // o retorno de chamada de notificação será invocado novamente.watch(...s: Signal[]): void;// Remove esses sinais do conjunto observado (por exemplo, para um efeito que está descartado)unwatch(...s : Sinal[]): void;// Retorna o conjunto de fontes no conjunto do Observador que ainda estão sujas, ou é um sinal computado // com uma fonte que está suja ou pendente e ainda não foi reavaliadagetPending(): Signal[];} // Ganchos para observar sendo observado ou não sendo mais observadovar observado: Symbol;var unwatched: Symbol;}interface SignalOptions<T> {// Função de comparação personalizada entre o valor antigo e o novo. Padrão: Object.is.// O sinal é passado como o valor this para context.equals?: (this: Signal<T>, t: T, t2: T) => boolean;// Retorno de chamada chamado quando isWatched se torna verdadeiro, se anteriormente era falso[Signal.subtle.watched]?: (this: Signal<T>) => void;// Retorno de chamada chamado sempre que isWatched torna-se falso, se foi anteriormente true[Signal.subtle.unwatched]?: (isto: Signal<T>) => void;}}
Um sinal representa uma célula de dados que pode mudar com o tempo. Os sinais podem ser de "estado" (apenas um valor definido manualmente) ou "computados" (uma fórmula baseada em outros sinais).
Os Sinais Computados funcionam rastreando automaticamente quais outros Sinais são lidos durante sua avaliação. Quando um cálculo é lido, ele verifica se alguma de suas dependências registradas anteriormente foi alterada e, em caso afirmativo, reavalia-se. Quando vários sinais computados são aninhados, toda a atribuição do rastreamento vai para o mais interno.
Os Sinais Computados são preguiçosos, ou seja, baseados em pull: eles só são reavaliados quando são acessados, mesmo que uma de suas dependências tenha sido alterada anteriormente.
O retorno de chamada passado para Sinais computados geralmente deve ser "puro" no sentido de ser uma função determinística e livre de efeitos colaterais dos outros Sinais que ele acessa. Ao mesmo tempo, o tempo de chamada do retorno de chamada é determinístico, permitindo que os efeitos colaterais sejam usados com cuidado.
Os sinais apresentam cache/memoização proeminente: tanto os sinais de estado quanto os calculados lembram seu valor atual e apenas acionam o recálculo dos sinais computados que os referenciam se eles realmente mudarem. Uma comparação repetida de valores antigos e novos nem é necessária - a comparação é feita uma vez quando o sinal de origem é redefinido/reavaliado, e o mecanismo de sinal monitora quais coisas que fazem referência a esse sinal não foram atualizadas com base no novo valor ainda. Internamente, isso geralmente é representado por meio de "coloração de gráfico", conforme descrito em (postagem do blog de Milo).
Os sinais computados rastreiam suas dependências dinamicamente - cada vez que são executados, eles podem acabar dependendo de coisas diferentes, e esse conjunto preciso de dependências é mantido atualizado no gráfico do Signal. Isso significa que se você tiver uma dependência necessária em apenas uma ramificação e o cálculo anterior tiver levado a outra ramificação, uma alteração nesse valor temporariamente não utilizado não fará com que o sinal calculado seja recalculado, mesmo quando puxado.
Ao contrário do JavaScript Promises, tudo no Signals é executado de forma síncrona:
Definir um sinal para um novo valor é síncrono e isso é refletido imediatamente ao ler qualquer sinal computado que dependa dele posteriormente. Não há lote integrado dessa mutação.
A leitura de sinais computados é síncrona – seu valor está sempre disponível.
O retorno de chamada notify
em Watchers, conforme explicado abaixo, é executado de forma síncrona, durante a chamada .set()
que o acionou (mas após a conclusão da coloração do gráfico).
Assim como as promessas, os sinais podem representar um estado de erro: se o retorno de chamada de um sinal computado for lançado, esse erro será armazenado em cache como outro valor e relançado sempre que o sinal for lido.
Uma instância Signal
representa a capacidade de ler um valor que muda dinamicamente cujas atualizações são rastreadas ao longo do tempo. Também inclui implicitamente a capacidade de assinar o Sinal, implicitamente através de um acesso rastreado de outro Sinal computado.
A API aqui foi projetada para corresponder ao consenso aproximado do ecossistema entre uma grande fração de bibliotecas Signal no uso de nomes como "sinal", "computado" e "estado". No entanto, o acesso aos sinais computados e de estado é feito por meio de um método .get()
, que discorda de todas as APIs de sinal populares, que usam um acessador estilo .value
ou sintaxe de chamada signal()
.
A API foi projetada para reduzir o número de alocações, para tornar os Sinais adequados para incorporação em estruturas JavaScript e, ao mesmo tempo, alcançar desempenho igual ou melhor do que os Sinais personalizados da estrutura existente. Isto implica:
Os sinais de estado são um único objeto gravável, que pode ser acessado e definido a partir da mesma referência. (Veja as implicações abaixo na seção "Separação de capacidades".)
Tanto os sinais de estado quanto os sinais computados são projetados para serem subclassíveis, para facilitar a capacidade das estruturas de adicionar propriedades adicionais por meio de campos de classe públicos e privados (bem como métodos para usar esse estado).
Vários retornos de chamada (por exemplo, equals
, o retorno de chamada calculado) são chamados com o Signal relevante como o valor this
para o contexto, de modo que um novo fechamento não seja necessário por Signal. Em vez disso, o contexto pode ser salvo em propriedades extras do próprio sinal.
Algumas condições de erro impostas por esta API:
É um erro ler um cálculo recursivamente.
O retorno de chamada notify
de um Watcher não pode ler ou gravar nenhum sinal
Se o retorno de chamada de um Signal computado for lançado, os acessos subsequentes do Signal relançarão esse erro armazenado em cache, até que uma das dependências seja alterada e ela seja recalculada.
Algumas condições que não são aplicadas:
Sinais computados podem gravar em outros sinais, de forma síncrona dentro de seu retorno de chamada
O trabalho que é enfileirado por um retorno de chamada notify
do Watcher pode ler ou escrever sinais, tornando possível replicar antipadrões clássicos do React em termos de Sinais!
A interface Watcher
definida acima fornece a base para a implementação de APIs JS típicas para efeitos: retornos de chamada que são executados novamente quando outros sinais mudam, puramente por seu efeito colateral. A função effect
usada acima no exemplo inicial pode ser definida da seguinte forma:
// Esta função normalmente residiria em uma biblioteca/estrutura, não no código do aplicativo// NOTA: Esta lógica de agendamento é muito básica para ser útil. Não copie / cole.let pendente = falso; deixe w = new Signal.subtle.Watcher(() => {if (! pendente) {pendente = verdadeiro; filaMicrotask (() => {pendente = falso; s of w.getPending()) s.get();w.watch();});}});// Um sinal de efeito de efeito que é avaliado como cb, que agenda uma leitura // de si mesmo na fila de microtarefas sempre que um de seus dependências podem mudar efeito de função de exportação (cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch(c);c.get ();return() => { destruidor?.(); w.unwatch(c) };}
A API Signal não inclui nenhuma função integrada como effect
. Isso ocorre porque o agendamento de efeitos é sutil e muitas vezes está vinculado a ciclos de renderização de estrutura e outros estados ou estratégias específicas de estrutura de alto nível aos quais JS não tem acesso.
Percorrendo as diferentes operações usadas aqui: O retorno de chamada notify
passado para o construtor Watcher
é a função que é chamada quando o Signal passa de um estado "limpo" (onde sabemos que o cache está inicializado e válido) para um estado "verificado" ou "sujo". " estado (onde o cache pode ou não ser válido porque pelo menos um dos estados dos quais depende recursivamente foi alterado).
As chamadas para notify
são acionadas por uma chamada para .set()
em algum sinal de estado. Esta chamada é síncrona: acontece antes do retorno de .set
. Mas não há necessidade de se preocupar com esse retorno de chamada observando o gráfico do Signal em um estado meio processado, porque durante um retorno de chamada notify
, nenhum sinal pode ser lido ou gravado, mesmo em uma chamada untrack
. Como notify
é chamado durante .set()
, ele está interrompendo outro thread de lógica, que pode não estar completo. Para ler ou escrever sinais de notify
, agende o trabalho para ser executado mais tarde, por exemplo, anotando o sinal em uma lista para ser acessado posteriormente, ou com queueMicrotask
como acima.
Observe que é perfeitamente possível usar Sinais de forma eficaz sem Symbol.subtle.Watcher
agendando a pesquisa de Sinais computados, como faz o Glimmer. No entanto, muitas estruturas descobriram que muitas vezes é útil ter essa lógica de agendamento executada de forma síncrona, portanto, a API Signals a inclui.
Os sinais computados e de estado são coletados como lixo como qualquer valor JS. Mas os Observadores têm uma maneira especial de manter as coisas vivas: quaisquer sinais que sejam observados por um Observador serão mantidos vivos enquanto qualquer um dos estados subjacentes estiver acessível, pois estes podem acionar uma chamada notify
futura (e então um futuro .get()
). Por esse motivo, lembre-se de chamar Watcher.prototype.unwatch
para limpar os efeitos.
Signal.subtle.untrack
é uma saída de emergência que permite a leitura de sinais sem rastrear essas leituras. Esta capacidade não é segura porque permite a criação de Sinais computados cujo valor depende de outros Sinais, mas que não são atualizados quando esses Sinais mudam. Deve ser utilizado quando os acessos não rastreados não alterarão o resultado do cálculo.
Esses recursos podem ser adicionados posteriormente, mas não estão incluídos no rascunho atual. A sua omissão deve-se à falta de consenso estabelecido no espaço de design entre os frameworks, bem como à capacidade demonstrada de contornar a sua ausência com mecanismos além da noção de Sinais descrita neste documento. No entanto, infelizmente, a omissão limita o potencial de interoperabilidade entre estruturas. À medida que os protótipos de Sinais descritos neste documento forem produzidos, haverá um esforço para reexaminar se essas omissões foram a decisão apropriada.
Assíncrono : Os sinais estão sempre disponíveis de forma síncrona para avaliação, neste modelo. No entanto, é frequentemente útil ter certos processos assíncronos que levam à configuração de um sinal e ter uma compreensão de quando um sinal ainda está "carregando". Uma maneira simples de modelar o estado de carregamento é com exceções, e o comportamento de armazenamento em cache de exceções dos sinais computados se compõe razoavelmente com esta técnica. Técnicas aprimoradas são discutidas na edição nº 30.
Transações : para transições entre visualizações, geralmente é útil manter um estado ativo para os estados "de" e "para". O estado "para" é renderizado em segundo plano, até que esteja pronto para trocar (confirmar a transação), enquanto o estado "de" permanece interativo. Manter ambos os estados ao mesmo tempo requer uma "bifurcação" do estado do gráfico do sinal e pode até ser útil para suportar múltiplas transições pendentes de uma só vez. Discussão na edição nº 73.
Alguns possíveis métodos de conveniência também são omitidos.
Esta proposta está na agenda TC39 de abril de 2024 para a Fase 1. Atualmente pode ser considerada como a “Etapa 0”.
Está disponível um polyfill para esta proposta, com alguns testes básicos. Alguns autores de frameworks começaram a experimentar a substituição desta implementação de sinal, mas esse uso está em um estágio inicial.
Os colaboradores da proposta Signal querem ser especialmente conservadores na forma como levamos esta proposta adiante, para não cairmos na armadilha de enviar algo que acabamos nos arrependendo e não usando de fato. Nosso plano é realizar as seguintes tarefas extras, não exigidas pelo processo TC39, para garantir que esta proposta esteja no caminho certo:
Antes de propor para a Fase 2, planejamos:
Desenvolva múltiplas implementações de polyfill de nível de produção que sejam sólidas, bem testadas (por exemplo, passando em testes de várias estruturas, bem como testes no estilo test262) e competitivas em termos de desempenho (conforme verificado com um conjunto completo de benchmark de sinal/estrutura).
Integrar a API Signal proposta em um grande número de frameworks JS que consideramos um tanto representativos, e algumas aplicações de grande porte trabalham com esta base. Teste se funciona de forma eficiente e correta nesses contextos.
Ter um conhecimento sólido sobre o espaço de possíveis extensões da API e concluir quais (se houver) devem ser adicionadas a esta proposta.
Esta seção descreve cada uma das APIs expostas ao JavaScript, em termos dos algoritmos que elas implementam. Isso pode ser pensado como uma protoespecificação e é incluído neste ponto inicial para definir um conjunto possível de semântica, ao mesmo tempo que está muito aberto a mudanças.
Alguns aspectos do algoritmo:
A ordem de leitura de Signals dentro de um calculado é significativa e é observável na ordem em que certos retornos de chamada (qual Watcher
é invocado, equals
, o primeiro parâmetro para new Signal.Computed
e os retornos de chamada watched
/ unwatched
) são executados. Isto significa que as fontes de um sinal computado devem ser armazenadas ordenadas.
Todos esses quatro retornos de chamada podem gerar exceções, e essas exceções são propagadas de maneira previsível para o código JS de chamada. As exceções não interrompem a execução deste algoritmo nem deixam o gráfico em um estado meio processado. Para erros lançados no retorno de chamada notify
de um Watcher, essa exceção é enviada para a chamada .set()
que a acionou, usando um AggregateError se múltiplas exceções foram lançadas. Os outros (incluindo watched
/ unwatched
?) são armazenados no valor do Sinal, para serem relançados quando lidos, e tal Sinal relançado pode ser marcado como ~clean~
como qualquer outro com um valor normal.
É tomado cuidado para evitar circularidades em casos de sinais computados que não são "observados" (sendo observados por qualquer Observador), para que possam ser coletados como lixo independentemente de outras partes do gráfico do sinal. Internamente, isso pode ser implementado com um sistema de números de geração que são sempre coletados; observe que as implementações otimizadas também podem incluir números de geração locais por nó ou evitar o rastreamento de alguns números nos sinais observados.
Os algoritmos de sinal precisam fazer referência a determinado estado global. Este estado é global para todo o thread, ou "agente".
computing
: O sinal computado ou de efeito mais interno atualmente sendo reavaliado devido a uma chamada .get
ou .run
ou null
. Inicialmente null
.
frozen
: Booleano que indica se há um retorno de chamada em execução que requer que o gráfico não seja modificado. Inicialmente false
.
generation
: um número inteiro incremental, começando em 0, usado para rastrear a atualidade de um valor, evitando circularidades.
Signal
Signal
é um objeto comum que serve como namespace para classes e funções relacionadas ao Signal.
Signal.subtle
é um objeto de namespace interno semelhante.
Signal.State
Signal.State
value
: O valor atual do sinal de estado
equals
: A função de comparação usada ao alterar valores
watched
: O retorno de chamada a ser chamado quando o sinal for observado por um efeito
unwatched
: O retorno de chamada a ser chamado quando o sinal não é mais observado por um efeito
sinks
: Conjunto de sinais observados que dependem deste
Signal.State(initialValue, options)
Defina value
deste sinal como initialValue
.
Defina este sinal equals
a options?.equals
Definir este sinal watched
como opções?.[Signal.subtle.watched]
Definir este sinal unwatched
como opções?.[Signal.subtle.unwatched]
Defina sinks
deste sinal para o conjunto vazio
Signal.State.prototype.get()
Se frozen
for verdadeiro, lance uma exceção.
Se computing
não for undefined
, adicione este sinal ao conjunto sources
da computing
.
NOTA: Não adicionamos computing
ao conjunto de sinks
deste sinal até que ele seja assistido por um observador.
Retorne value
deste sinal.
Signal.State.prototype.set(newValue)
Se o contexto de execução atual estiver frozen
, lance uma exceção.
Execute o algoritmo "definir valor do sinal" com este sinal e o primeiro parâmetro para o valor.
Se esse algoritmo retornou ~clean~
, retorne indefinido.
Defina o state
de todos os sinks
deste sinal como (se for um sinal computado) ~dirty~
se eles estavam limpos anteriormente, ou (se for um observador) ~pending~
se estava anteriormente ~watching~
.
Defina o state
de todas as dependências de sinal computado dos coletores (recursivamente) para ~checked~
se eles foram anteriormente ~clean~
(ou seja, deixe marcações sujas no lugar), ou para Watchers, ~pending~
se anteriormente ~watching~
.
Para cada ~watching~
encontrado anteriormente naquela pesquisa recursiva, então em primeira ordem em profundidade,
Defina frozen
como verdadeiro.
Chamando seu retorno de chamada notify
(deixando de lado qualquer exceção lançada, mas ignorando o valor de retorno de notify
).
Restaurar frozen
para falso.
Defina o state
do Watcher como ~waiting~
.
Se alguma exceção foi lançada nos retornos de notify
, propague-a para o chamador após a execução de todos os retornos de notify
. Se houver várias exceções, empacote-as em um AggregateError e jogue-o.
Retorno indefinido.
Signal.Computed
Signal.Computed
O state
de um sinal computado pode ser um dos seguintes:
~clean~
: O valor do Signal está presente e é conhecido por não ser obsoleto.
~checked~
: Uma fonte (indireta) deste sinal foi alterada; este sinal tem um valor, mas pode estar obsoleto. Se está obsoleto ou não, só será conhecido quando todas as fontes imediatas tiverem sido avaliadas.
~computing~
: O retorno de chamada deste Signal está sendo executado como efeito colateral de uma chamada .get()
.
~dirty~
: Ou este sinal tem um valor que é sabidamente obsoleto ou nunca foi avaliado.
O gráfico de transição é o seguinte:
stateDiagram-v2
[*] --> sujo
sujo -> computação: [4]
computação -> limpo: [5]
limpo -> sujo: [2]
limpar --> verificado: [3]
verificado -> limpo: [6]
verificado --> sujo: [1]
CarregandoAs transições são:
Número | De | Para | Doença | Algoritmo |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | Uma fonte imediata deste sinal, que é um sinal computado, foi avaliada e seu valor foi alterado. | Algoritmo: recalcular sinal computado sujo |
2 | ~clean~ | ~dirty~ | Foi definida uma fonte imediata deste sinal, que é um Estado, com um valor que não é igual ao seu valor anterior. | Método: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | Foi definida uma fonte recursiva, mas não imediata, deste sinal, que é um Estado, com um valor que não é igual ao seu valor anterior. | Método: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | Estamos prestes a executar o callback . | Algoritmo: recalcular sinal computado sujo |
5 | ~computing~ | ~clean~ | O callback concluiu a avaliação e retornou um valor ou gerou uma exceção. | Algoritmo: recalcular sinal computado sujo |
6 | ~checked~ | ~clean~ | Todas as fontes imediatas deste sinal foram avaliadas e todas foram descobertas inalteradas, por isso agora sabemos que não estamos obsoletos. | Algoritmo: recalcular sinal computado sujo |
Signal.Computed
value
: O valor armazenado em cache anterior do Signal, ou ~uninitialized~
para um Signal computado nunca lido. O valor pode ser uma exceção que é lançada novamente quando o valor é lido. Sempre undefined
para sinais de efeito.
state
: pode ser ~clean~
, ~checked~
, ~computing~
ou ~dirty~
.
sources
: um conjunto ordenado de sinais dos quais este sinal depende.
sinks
: um conjunto ordenado de sinais que dependem deste sinal.
equals
: O método equals fornecido nas opções.
callback
: O retorno de chamada que é chamado para obter o valor do sinal calculado. Defina como o primeiro parâmetro passado ao construtor.
Signal.Computed
Os conjuntos construtores
callback
para seu primeiro parâmetro
equals
com base nas opções, padronizando Object.is
se ausente
state
para ~dirty~
value
para ~uninitialized~
Com AsyncContext, o retorno de chamada passado para new Signal.Computed
fecha o instantâneo de quando o construtor foi chamado e restaura esse instantâneo durante sua execução.
Signal.Computed.prototype.get
Se o contexto de execução atual estiver frozen
ou se este Sinal tiver o estado ~computing~
, ou se este sinal for um Efeito e computing
um Sinal computado, lance uma exceção.
Se computing
não for null
, adicione este Sinal ao conjunto sources
da computing
.
NOTA: Não adicionamos computing
ao conjunto de sinks
deste sinal até/a menos que ele seja observado por um observador.
Se o estado deste sinal for ~dirty~
ou ~checked~
: Repita os seguintes passos até que este sinal esteja ~clean~
:
Recorra através de sources
para encontrar a fonte recursiva mais profunda, mais à esquerda (isto é, observada mais cedo) que é um sinal computado marcado como ~dirty~
(cortando a pesquisa ao atingir um sinal computado ~clean~
e incluindo este sinal computado como a última coisa para pesquisar).
Execute o algoritmo "recalcular sinal computado sujo" nesse sinal.
Neste ponto, o estado deste sinal será ~clean~
, e nenhuma fonte recursiva será ~dirty~
ou ~checked~
. Retorne o value
do sinal. Se o valor for uma exceção, repita essa exceção.
Signal.subtle.Watcher
Signal.subtle.Watcher
O state
de um Observador pode ser um dos seguintes:
~waiting~
: O retorno de chamada notify
foi executado ou o Watcher é novo, mas não está monitorando ativamente nenhum sinal.
~watching~
: O Watcher está observando ativamente os sinais, mas nenhuma mudança aconteceu ainda que exigiria um retorno de chamada notify
.
~pending~
: Uma dependência do Watcher foi alterada, mas o retorno de chamada notify
ainda não foi executado.
O gráfico de transição é o seguinte:
stateDiagram-v2
[*] --> esperando
esperando --> assistindo: [1]
assistindo -> esperando: [2]
assistindo --> pendente: [3]
pendente --> aguardando: [4]
CarregandoAs transições são:
Número | De | Para | Doença | Algoritmo |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | O método watch do Watcher foi chamado. | Método: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | O método unwatch do Watcher foi chamado e o último sinal observado foi removido. | Método: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | Um sinal observado pode ter mudado de valor. | Método: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | O retorno de chamada notify foi executado. | Método: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
state
: pode ser ~watching~
, ~pending~
ou ~waiting~
signals
: um conjunto ordenado de sinais que este observador está observando
notifyCallback
: o retorno de chamada que é chamado quando algo muda. Defina como o primeiro parâmetro passado ao construtor.
new Signal.subtle.Watcher(callback)
state
está definido como ~waiting~
.
Inicialize signals
como um conjunto vazio.
notifyCallback
é definido como o parâmetro de retorno de chamada.
Com AsyncContext, o retorno de chamada passado para new Signal.subtle.Watcher
não fecha o instantâneo de quando o construtor foi chamado, de modo que as informações contextuais em torno da gravação ficam visíveis.
Signal.subtle.Watcher.prototype.watch(...signals)
Se frozen
for verdadeiro, lance uma exceção.
Se algum dos argumentos não for um sinal, lance uma exceção.
Anexe todos os argumentos ao final dos signals
deste objeto.
Para cada sinal recentemente assistido, na ordem da esquerda para a direita,
Adicione este observador como um sink
para esse sinal.
Se este foi o primeiro coletor, recorra às fontes para adicionar esse sinal como um coletor.
Defina frozen
como verdadeiro.
Chame o retorno de chamada watched
, se existir.
Restaurar frozen
para falso.
Se o state
do sinal for ~waiting~
, defina-o como ~watching~
.
Signal.subtle.Watcher.prototype.unwatch(...signals)
Se frozen
for verdadeiro, lance uma exceção.
Se algum dos argumentos não for um sinal ou não estiver sendo observado por este observador, lance uma exceção.
Para cada sinal nos argumentos, na ordem da esquerda para a direita,
Remova esse sinal do conjunto signals
deste Observador.
Remova este Observador do conjunto sink
daquele Sinal.
Se o conjunto de sink
desse sinal ficar vazio, remova esse sinal como coletor de cada uma de suas fontes.
Defina frozen
como verdadeiro.
Chame o retorno de chamada unwatched
, se existir.
Restaurar frozen
para falso.
Se o observador agora não tiver signals
e seu state
for ~watching~
, defina-o como ~waiting~
.
Signal.subtle.Watcher.prototype.getPending()
Retorna um Array contendo o subconjunto de signals
que são Sinais Computados nos estados ~dirty~
ou ~pending~
.
Signal.subtle.untrack(cb)
Seja c
o estado computing
atual do contexto de execução.
Defina computing
como nula.
Ligue para cb
.
Restaure computing
para c
(mesmo que cb
tenha lançado uma exceção).
Retorna o valor de retorno de cb
(relançando qualquer exceção).
Nota: untrack não tira você do estado frozen
, que é mantido estritamente.
Signal.subtle.currentComputed()
Retorne o valor computing
atual.
Limpe o conjunto sources
deste sinal e remova-o dos conjuntos sinks
dessas fontes.
Salve o valor computing
anterior e defina computing
para este sinal.
Defina o estado deste sinal para ~computing~
.
Execute o retorno de chamada deste sinal calculado, usando este sinal como o valor this. Salve o valor de retorno e, se o retorno de chamada gerar uma exceção, armazene-o para relançamento.
Restaure o valor computing
anterior.
Aplique o algoritmo "definir valor do sinal" ao valor de retorno do retorno de chamada.
Defina o estado deste sinal para ~clean~
.
Se esse algoritmo retornou ~dirty~
: marque todos os sinks deste Signal como ~dirty~
(anteriormente, os sinks podem ter sido uma mistura de verificados e sujos). (Ou, se isso não for observado, adote um número de nova geração para indicar sujeira, ou algo parecido.)
Caso contrário, esse algoritmo retornou ~clean~
: Neste caso, para cada coletor ~checked~
deste Signal, se todas as fontes desse Signal estiverem limpas, então marque esse Signal como ~clean~
também. Aplique esta etapa de limpeza a outros coletores de forma recursiva, a quaisquer sinais recém-limpos que tenham verificado os coletores. (Ou, se não for observado, indique de alguma forma o mesmo, para que a limpeza possa prosseguir preguiçosamente.)
Se este algoritmo recebeu um valor (em oposição a uma exceção para relançamento, do algoritmo de recálculo de sinal computado sujo):
Chame a função equals
deste Signal, passando como parâmetros o value
atual, o novo valor e este Signal. Se uma exceção for lançada, salve essa exceção (para relançar quando lida) como o valor do Signal e continue como se o retorno de chamada tivesse retornado falso.
Se essa função retornou verdadeiro, retorne ~clean~
.
Defina o value
deste sinal para o parâmetro.
Retorno ~dirty~
P : Não é um pouco cedo para padronizar algo relacionado aos Sinais, quando eles começaram a ser a novidade em 2022? Não deveríamos dar-lhes mais tempo para evoluir e se estabilizar?
R : O estado atual dos Signals em frameworks web é o resultado de mais de 10 anos de desenvolvimento contínuo. À medida que o investimento aumenta, como tem acontecido nos últimos anos, quase todas as estruturas da web estão se aproximando de um modelo central de Sinais muito semelhante. Esta proposta é o resultado de um exercício de design partilhado entre um grande número de atuais líderes em frameworks web, e não será levada à padronização sem a validação desse grupo de especialistas do domínio em vários contextos.
P : Os sinais integrados podem ser usados por frameworks, dada a sua forte integração com renderização e propriedade?
R : As partes que são mais específicas da estrutura tendem a estar na área de efeitos, programação e propriedade/descarte, que esta proposta não tenta resolver. Nossa primeira prioridade com a prototipagem de sinais de rastreamento de padrões é validar se eles podem ficar "abaixo" das estruturas existentes de forma compatível e com bom desempenho.
P : A API Signal deve ser usada diretamente por desenvolvedores de aplicativos ou envolvida por estruturas?
R : Embora esta API possa ser usada diretamente por desenvolvedores de aplicativos (pelo menos a parte que não está no namespace Signal.subtle
), ela não foi projetada para ser especialmente ergonômica. Em vez disso, as necessidades dos autores de bibliotecas/estruturas são prioridades. Espera-se que a maioria das estruturas envolva até mesmo as APIs básicas Signal.State
e Signal.Computed
com algo que expresse sua inclinação ergonômica. Na prática, normalmente é melhor usar Sinais por meio de uma estrutura, que gerencia recursos mais complicados (por exemplo, Watcher, untrack
), bem como gerencia a propriedade e o descarte (por exemplo, descobrir quando os sinais devem ser adicionados e removidos dos observadores) e agendando a renderização para DOM - esta proposta não tenta resolver esses problemas.
P : Preciso eliminar os sinais relacionados a um widget quando esse widget for destruído? Qual é a API para isso?
R : A operação de desmontagem relevante aqui é Signal.subtle.Watcher.prototype.unwatch
. Apenas os sinais assistidos precisam ser limpos (deixando de assisti-los), enquanto os sinais não assistidos podem ser coletados automaticamente como lixo.
P : Os Signals funcionam com VDOM ou diretamente com o HTML DOM subjacente?
R : Sim! Os sinais são independentes da tecnologia de renderização. As estruturas JavaScript existentes que usam construções semelhantes ao Signal integram-se ao VDOM (por exemplo, Preact), ao DOM nativo (por exemplo, Solid) e a uma combinação (por exemplo, Vue). O mesmo será possível com Sinais integrados.
P : Será ergonômico usar Signals no contexto de estruturas baseadas em classes como Angular e Lit? E quanto a estruturas baseadas em compilador como Svelte?
R : Os campos de classe podem ser baseados em Signal com um decorador de acessador simples, conforme mostrado no leia-me do polyfill do Signal. Os sinais estão intimamente alinhados às Runas do Svelte 5 - é simples para um compilador transformar runas na API Signal definida aqui e, na verdade, é isso que o Svelte 5 faz internamente (mas com sua própria biblioteca de Sinais).
P : Os sinais funcionam com SSR? Hidratação? Retomabilidade?
R : Sim. Qwik usa Signals com bons resultados com ambas as propriedades, e outras estruturas têm outras abordagens bem desenvolvidas para hidratação com Signals com diferentes compensações. Achamos que é possível modelar os sinais recuperáveis do Qwik usando um sinal de estado e um sinal computado conectados, e planejamos provar isso em código.
P : Os Signals funcionam com fluxo de dados unidirecional como o React?
R : Sim, os sinais são um mecanismo para fluxo de dados unidirecional. As estruturas de UI baseadas em sinais permitem expressar sua visão como uma função do modelo (onde o modelo incorpora sinais). Um gráfico de estado e sinais calculados é acíclico por construção. Também é possível recriar antipadrões React dentro de Signals (!), por exemplo, o Signal equivalente a um setState
dentro de useEffect
é usar um Watcher para agendar uma gravação em um sinal State.
P : Como os sinais se relacionam com sistemas de gerenciamento de estado como o Redux? Os sinais encorajam o estado não estruturado?
R : Os sinais podem formar uma base eficiente para abstrações de gerenciamento de estado semelhantes a lojas. Um padrão comum encontrado em vários frameworks é um objeto baseado em um proxy que representa internamente propriedades usando sinais, por exemplo, Vue reactive()
ou armazenamentos sólidos. Esses sistemas permitem o agrupamento flexível de estados no nível certo de abstração para a aplicação específica.
P : O que o Signals oferece que Proxy
não suporta atualmente?
R : Proxies e Sinais são complementares e combinam bem. Os proxies permitem interceptar operações superficiais de objetos e os sinais coordenam um gráfico de dependência (de células). Apoiar um proxy com sinais é uma ótima maneira de criar uma estrutura reativa aninhada com ótima ergonomia.
Neste exemplo, podemos usar um proxy para fazer com que o sinal tenha uma propriedade getter e setter em vez de usar os métodos get
e set
:
const a = novo Signal.State(0);const b = novo Proxy(a, { get (alvo, propriedade, receptor) {if (propriedade === 'valor') { return target.get():} } set(alvo, propriedade, valor, receptor) {if (propriedade === 'valor') { alvo.set(valor)!} }});// uso em um contexto reativo hipotético:<template> {b.valor} <botão onclick={() => {b.valor++; }}>alterar</button></template>
ao usar um renderizador otimizado para reatividade refinada, clicar no botão fará com que a célula b.value
seja atualizada.
Ver:
exemplos de estruturas reativas aninhadas criadas com sinais e proxies: signal-utils
exemplo de implementações anteriores mostrando a relação entre dados reativos e proxies: tracked-built-ins
discussão.
P : Os sinais são baseados em push ou pull?
R : A avaliação dos sinais computados é baseada em pull: os sinais computados são avaliados apenas quando .get()
é chamado, mesmo que o estado subjacente tenha mudado muito antes. Ao mesmo tempo, alterar um sinal de estado pode acionar imediatamente um retorno de chamada do Observador, "empurrando" a notificação. Portanto, os Sinais podem ser considerados uma construção "push-pull".
P : Os sinais introduzem não determinismo na execução do JavaScript?
R : Não. Por um lado, todas as operações do Signal têm semântica e ordenação bem definidas e não diferirão entre implementações compatíveis. Num nível superior, os Sinais seguem um certo conjunto de invariantes, em relação aos quais são "som". Um sinal computado sempre observa o gráfico do sinal em um estado consistente e sua execução não é interrompida por outro código com mutação de sinal (exceto por coisas que ele chama a si mesmo). Veja a descrição acima.
P : Quando escrevo em um sinal de estado, quando é agendada a atualização do sinal computado?
R : Não está programado! O sinal computado se recalculará na próxima vez que alguém o ler. De forma síncrona, um retorno de chamada notify
do Watcher pode ser chamado, permitindo que os frameworks agendem uma leitura no momento que acharem apropriado.
P : Quando as gravações nos sinais de estado entram em vigor? Imediatamente ou eles são agrupados?
R : As gravações nos sinais de estado são refletidas imediatamente - na próxima vez que um sinal computado que depende do sinal de estado for lido, ele se recalculará se necessário, mesmo na linha de código imediatamente seguinte. Porém, a preguiça inerente a este mecanismo (que os sinais computados só são computados quando lidos) faz com que, na prática, os cálculos possam acontecer em lote.
P : O que significa para o Signals permitir uma execução "sem falhas"?
R : Os modelos anteriores baseados em push para reatividade enfrentavam um problema de computação redundante: se uma atualização para um sinal de estado fizer com que o sinal computado seja executado com entusiasmo, em última análise, isso poderá enviar uma atualização para a IU. Mas essa gravação na IU pode ser prematura, se houver outra alteração no estado de origem do sinal antes do próximo quadro. Às vezes, valores intermediários imprecisos eram mostrados aos usuários finais devido a tais falhas. Os sinais evitam essa dinâmica por serem baseados em pull, em vez de baseados em push: no momento em que o framework agenda a renderização da UI, ele puxará as atualizações apropriadas, evitando desperdício de trabalho tanto na computação quanto na gravação no DOM.
P : O que significa sinais “com perdas”?
R : Este é o outro lado da execução sem falhas: os sinais representam uma célula de dados – apenas o valor atual imediato (que pode mudar), não um fluxo de dados ao longo do tempo. Portanto, se você escrever em um sinal de estado duas vezes seguidas, sem fazer mais nada, a primeira gravação será "perdida" e nunca será vista por nenhum sinal ou efeito computado. Isto é entendido como um recurso e não um bug - outras construções (por exemplo, iteráveis assíncronos, observáveis) são mais apropriadas para fluxos.
P : Os Signals nativos serão mais rápidos do que as implementações existentes do JS Signal?
R : Esperamos que sim (por um pequeno fator constante), mas isso ainda precisa ser provado no código. Os mecanismos JS não são mágicos e, em última análise, precisarão implementar os mesmos tipos de algoritmos que as implementações JS de Signals. Veja a seção acima sobre desempenho.
P : Por que esta proposta não inclui uma função effect()
, quando os efeitos são necessários para qualquer uso prático dos Sinais?
R : Os efeitos estão inerentemente vinculados ao agendamento e ao descarte, que são gerenciados por estruturas e fora do escopo desta proposta. Em vez disso, esta proposta inclui a base para implementar efeitos por meio da API Signal.subtle.Watcher
de nível mais baixo.
P : Por que as assinaturas são automáticas em vez de fornecerem uma interface manual?
R : A experiência mostra que as interfaces de assinatura manual para reatividade não são ergonômicas e estão sujeitas a erros. O rastreamento automático é mais combinável e é um recurso central do Signals.
P : Por que o retorno de chamada do Watcher
é executado de forma síncrona, em vez de agendado em uma microtarefa?
R : Como o retorno de chamada não pode ler ou gravar sinais, não há problemas causados ao chamá-lo de forma síncrona. Um retorno de chamada típico adicionará um sinal a um array para ser lido mais tarde ou marcará um bit em algum lugar. É desnecessário e pouco prático criar uma microtarefa separada para todos esses tipos de ações.
P : Faltam algumas coisas interessantes nesta API que minha estrutura favorita oferece, o que torna mais fácil programar com Signals. Isso também pode ser adicionado ao padrão?
R : Talvez. Várias extensões ainda estão em consideração. Registre um problema para levantar a discussão sobre qualquer recurso ausente que você considere importante.
P : Esta API pode ser reduzida em tamanho ou complexidade?
R : Definitivamente, o objetivo é manter essa API mínima, e tentamos fazer isso com o que é apresentado acima. Se você tiver ideias para mais coisas que podem ser removidas, registre um problema para discutir.
P : Não deveríamos começar o trabalho de padronização nesta área com um conceito mais primitivo, como observáveis?
R : Observáveis podem ser uma boa ideia para algumas coisas, mas não resolvem os problemas que os Sinais pretendem resolver. Conforme descrito acima, observáveis ou outros mecanismos de publicação/assinatura não são uma solução completa para muitos tipos de programação de UI, devido ao excesso de trabalho de configuração propenso a erros para os desenvolvedores e ao desperdício de trabalho devido à falta de preguiça, entre outros problemas.
P : Por que os Sinais estão sendo propostos no TC39 em vez do DOM, visto que a maioria das aplicações são baseadas na web?
R : Alguns co-autores desta proposta estão interessados em ambientes de UI não-web como objetivo, mas hoje em dia, qualquer local pode ser adequado para isso, já que as APIs da web estão sendo implementadas com mais frequência fora da web. Em última análise, os Signals não precisam depender de nenhuma API DOM, então qualquer forma funciona. Se alguém tiver um forte motivo para a mudança deste grupo, informe-nos sobre o problema. Por enquanto, todos os contribuidores assinaram os acordos de propriedade intelectual do TC39 e o plano é apresentá-los ao TC39.
P : Quanto tempo levará até que eu possa usar os sinais padrão?
R : Um polyfill já está disponível, mas é melhor não confiar em sua estabilidade, pois esta API evolui durante seu processo de revisão. Em alguns meses ou um ano, um polyfill estável de alta qualidade e alto desempenho deverá ser utilizável, mas ainda estará sujeito a revisões do comitê e ainda não será padronizado. Seguindo a trajetória típica de uma proposta TC39, espera-se que leve pelo menos 2 a 3 anos, no mínimo, para que os Sinais estejam disponíveis nativamente em todos os navegadores, desde algumas versões anteriores, de modo que os polyfills não sejam necessários.
P : Como evitaremos a padronização do tipo errado de sinais muito cedo, como {{JS/recurso da web que você não gosta}}?
R : Os autores desta proposta planejam ir além com a prototipagem e a prova antes de solicitar o avanço de estágio no TC39. Consulte "Situação e plano de desenvolvimento" acima. Se você encontrar lacunas neste plano ou oportunidades de melhoria, registre um problema explicando.