3 pontos por GN⁺ 2024-04-01 | 1 comentários | Compartilhar no WhatsApp
  • A proposta JavaScript Signals do TC39 é uma direção inicial para padronizar primitivos reativos para rastrear com eficiência estado de UI e estado computado, e atualmente é um rascunho em Stage 1
  • A proposta foca mais na semântica central do grafo de Signals e no mecanismo de rastreamento automático que frameworks podem compartilhar do que em uma API de superfície voltada diretamente para desenvolvedores de aplicações
  • Signal.State, Signal.Computed e Signal.subtle.Watcher são as APIs principais, e os cálculos buscam avaliação preguiçosa, cache, rastreamento automático de dependências e execução sem glitches
  • O objetivo dos Signals embutidos é aumentar a interoperabilidade entre vários frameworks como Angular, Ember, MobX, Preact, Qwik, Solid, Svelte e Vue, além de abrir possibilidades para suporte de depuração e análise de desempenho em DevTools
  • O grupo da proposta pretende passar por vários polyfills de nível de produção, integração com frameworks, validação em aplicações de grande escala e benchmarks de desempenho antes do Stage 2, e a padronização pode levar pelo menos mais de 2 a 3 anos

Onde a proposta JavaScript Signals se encaixa

  • JavaScript Signals é apresentada como uma proposta Stage 1 no processo do TC39
  • O documento atual é uma tentativa de alinhar uma direção comum para o ecossistema JavaScript, de forma parecida com o que Promises/A+ representou antes da padronização de Promise no ES2015
  • Há um polyfill disponível para experimentação direta
  • Os champions da proposta e autores originais montaram o rascunho atual com base em contribuições de design de vários frameworks e bibliotecas

O problema que a padronização quer resolver

  • UIs complexas precisam armazenar valores, calcular, invalidar, sincronizar e enviar atualizações para a camada de visualização, e os Signals querem fornecer uma infraestrutura de gerenciamento de estado para esse trabalho repetitivo
  • Em um exemplo com Vanilla JS, counter, isEven, parity e render ficam diretamente acoplados, gerando os seguintes problemas
    • O estado e o sistema de renderização ficam fortemente acoplados
    • Mesmo quando parity não muda, como no caso em que counter vai de 2 para 4, ainda ocorrem cálculos e renderizações desnecessários
    • Quando diferentes partes da UI querem assinar apenas alguns entre counter, isEven e parity, gerenciar manualmente inscrição e cancelamento se torna complexo
    • Adicionar pub/sub em várias etapas aumenta boilerplate, bookkeeping de assinaturas e o risco de vazamentos de memória
  • O exemplo baseado em Signals usa Signal.State e Signal.Computed para tratar valores, cálculos e efeitos colaterais de forma unificada
    • Não exige inscrições manuais
    • Encontra automaticamente de quais Signals um Signal computado depende
    • Os cálculos só são executados quando o valor é explicitamente solicitado
    • O Signal computado guarda em cache o último valor

APIs principais da proposta

  • Signal<T> é definido como um valor legível com get(): T
  • Signal.State<T> é um Signal gravável
    • O construtor recebe um valor inicial e opções
    • O valor é lido com get() e alterado com set(t)
  • Signal.Computed<T> é um Signal computado baseado em outros Signals
    • É calculado a partir do valor retornado por um callback
    • Faz rastreamento automático de dependências
    • O valor é calculado de forma lazy e mantido em cache
  • Signal.subtle reúne APIs avançadas, mais próximas de autores de frameworks ou implementações de DevTools
    • untrack(cb) permite ler um Signal sem rastreamento
    • currentComputed() retorna o computed Signal atualmente em rastreamento
    • introspectSources, introspectSinks, hasSinks e hasSources são APIs para observar o grafo
    • Watcher é a base para implementar effects e agendamento em nível de framework ao detectar mudanças de Signals
  • SignalOptions<T> oferece suporte a uma função de comparação personalizada equals e hooks watched / unwatched

Como funciona e modelo de execução

  • Um Signal representa uma célula de dados que pode mudar ao longo do tempo, dividida em state ou computed
  • Um Signal computado registra automaticamente os Signals lidos durante sua execução e, quando for lido depois, verifica se dependências anteriores mudaram
  • O cálculo é baseado em pull
    • Mesmo que dependências mudem, ele não recalcula imediatamente
    • Ele recalcula quando necessário, no momento em que alguém o lê com .get()
  • Escritas em State Signals são refletidas de forma síncrona
    • Depois de .set(), se um computed Signal dependente for lido, ele recalcula imediatamente se necessário
    • Não há batching embutido
  • O callback notify de um Watcher pode ser executado de forma síncrona durante .set()
    • Porém, não é permitido ler nem escrever Signals dentro de notify
    • Operações reais de leitura e escrita devem ser agendadas depois
  • Se o callback de um computed Signal lançar uma exceção, essa exceção também fica em cache como se fosse um valor e é lançada novamente quando o Signal for relido

Motivação para a padronização

  • As implementações de Signals em cada framework têm seu próprio mecanismo de rastreamento automático, o que dificulta compartilhar modelos, componentes e bibliotecas entre frameworks
  • O objetivo da proposta é separar o modelo reativo da view de renderização
    • Isso evita que desenvolvedores precisem reescrever código não relacionado à UI ao trocar de tecnologia de renderização
    • Também permite criar em JavaScript modelos reativos compartilháveis em vários contextos
  • Em termos de desempenho e memória, o documento afirma que uma implementação embutida pode ser mais eficiente do que uma em JS por um pequeno fator constante, mas deixa claro que o engine não vai mudar algoritmos magicamente
  • Do ponto de vista de DevTools, Signals embutidos poderiam mostrar melhor as seguintes informações
    • A call stack de cadeias de computed Signals
    • O grafo de referências entre Signals
    • Relações de dependência necessárias para depurar uso de memória
  • Caso entrem na biblioteca padrão, também se esperam efeitos colaterais positivos, como redução do tamanho de bundle, melhora de estabilidade e qualidade, e formação de um vocabulário comum entre projetos

Objetivos e restrições de design

  • As capacidades centrais incluem Signal gravável, Signal computado, reação a estado dirty, agendamento próprio de framework, untrack e composição entre múltiplos codebases
  • Signals computados buscam ser glitch-free
    • Para evitar cálculos desnecessários, partes potencialmente dirty do grafo são executadas em ordem topológica
    • A proposta tenta eliminar cálculos duplicados
  • Não há um agendamento forçado embutido no estilo Promise, para que frameworks possam manter seu próprio modelo de scheduling
  • Para evitar uso indevido de callbacks de reação síncrona, a leitura e escrita de Signals são proibidas dentro de notify de um Watcher
  • untrack é tratado como uma rota de escape insegura
    • Se um Signal lido sem rastreamento influenciar o resultado do cálculo, mudanças nesse Signal podem não atualizar o computed Signal
  • A API prioriza servir de base para implementações de framework, e não foi desenhada especialmente para ser ergonômica para desenvolvedores de aplicações em geral

effect e Watcher

  • A proposta não inclui uma função embutida como effect()
  • O agendamento de effects se mistura com ciclo de renderização do framework, descarte e gestão de ownership, então a API padrão do JavaScript não tenta resolver isso diretamente
  • Em vez disso, Signal.subtle.Watcher fornece a base de baixo nível para implementar effects
    • notify é chamado quando dependências de um Signal observado mudam
    • getPending() permite verificar quais Signals ainda estão dirty
    • Effects que precisam de descarte devem ser limpos com unwatch
  • Um Signal observado por um Watcher pode continuar vivo enquanto seu estado interno estiver alcançável, então a limpeza de effects exige chamar Watcher.prototype.unwatch

Funcionalidades ausentes no rascunho atual

  • Async não está incluído no modelo atual
    • Signals são sempre tratados como valores avaliáveis de forma síncrona
    • Há alguma possibilidade de modelar estado de loading com exceções, e a discussão de melhorias está na Issue #30
  • Transactions também não estão incluídas
    • Para manter ao mesmo tempo os estados “from” e “to” em uma transição de tela, surge o problema de fazer fork do estado do grafo de Signals
    • A discussão relacionada está na Issue #73
  • Alguns convenience methods também estão ausentes do rascunho atual
  • Essas funcionalidades ficaram de fora por falta de consenso entre frameworks e porque podem ser contornadas em camadas superiores, mas podem ser reavaliadas depois dos protótipos

Plano de desenvolvimento e cronograma de padronização

  • A proposta entrou na pauta do TC39 Stage 1 em abril de 2024, e o documento explica que, na prática, ainda pode ser vista como algo próximo de Stage 0
  • Antes de propor o Stage 2, o plano inclui os seguintes trabalhos
    • Desenvolver várias implementações de polyfill em nível de produção
    • Testar em diferentes frameworks e passar em testes no estilo test262
    • Validar desempenho com um conjunto robusto de benchmarks de Signals/frameworks
    • Integrar a API proposta a vários frameworks JS representativos e a algumas aplicações de grande escala
    • Entender a capacidade de extensão da API e decidir o que deve ou não ser incluído
  • O grupo da proposta quer avançar de forma conservadora para evitar que uma forma inadequada de Signals seja padronizada cedo demais
  • O FAQ estima que Signals padronizados só devem ser usados amplamente em navegadores sem polyfill em pelo menos 2 a 3 anos
  • O polyfill atual já pode ser usado, mas o documento recomenda não depender da estabilidade da API, já que ela ainda pode mudar durante a revisão

Modelo de uso resumido no FAQ

  • Signals embutidos são independentes da tecnologia de renderização
    • O documento explica que isso funciona tanto para modelos como o VDOM do Preact, o DOM nativo do Solid e a abordagem híbrida do Vue
  • Para desenvolvedores de aplicações, em geral faz mais sentido usar Signals por meio de um framework
    • O framework gerencia Watcher, untrack, ownership, disposal e o agendamento da renderização do DOM
  • Eles podem ser usados junto com SSR, hydration e resumability
    • O Qwik já usa Signals com essas propriedades, e a proposta entende que os Signals resumable do Qwik podem ser modelados com uma combinação de State e Computed
  • Signals e Proxy são complementares
    • Proxy intercepta operações rasas em objetos, enquanto Signals coordenam o grafo de dependências de células de dados
    • Colocar Signals atrás de um Proxy pode tornar uma estrutura reativa aninhada mais ergonômica
  • Signals não são streams, e sim células que representam o valor atual
    • Se um State Signal receber duas escritas seguidas e nada mais acontecer, a primeira escrita pode não ser visível para computed Signals ou effects
    • O documento trata isso como uma característica no lado oposto da execução glitch-free e diz que, para streams, outras construções como async iterable ou observable são mais adequadas

1 comentários

 
GN⁺ 2024-04-01
Opiniões do Hacker News
  • Será que sou só eu que acho que o exemplo em JavaScript puro é, na verdade, mais fácil de ler e manipular?
    Dizem que “a configuração tem muito ruído e boilerplate”, mas o exemplo com signals parece igualmente barulhento e cheio de boilerplate, além de acrescentar um conceito novo difícil para iniciantes entenderem
    A ideia de que “se o counter muda de 2 para 4, a parity não muda, mas há cálculo e renderização desnecessários” soa como memoização prematura
    Se outra parte da UI quiser renderizar em resposta a atualizações do counter, é verdade que aquele exemplo strawman não é adequado; nesse caso, dá para usar outros métodos, como signals, tratamento de eventos ou um armazenamento central de estado (estilo Redux)
    Se outra parte da UI depende apenas de isEven ou parity, dá para mudar a abordagem caso isso seja a estrutura central do app, mas na maioria das vezes não é. A ideia de que “uma função render que depende apenas de parity precisa saber que deve assinar counter” também não é necessariamente um fardo injusto, e funções de cálculo puras têm a vantagem de tornar fácil identificar as entradas

    • Não entendo por que isso seria visto como memoização prematura. É apenas um exemplo reduzido a uma função simples, e é difícil acreditar que as pessoas inventaram esse caso de uso sem que ele jamais tenha sido necessário na prática
      Tentar padronizar signals, um conceito cada vez mais usado no desenvolvimento de UI, é algo louvável. Deixando de lado debates de detalhe, como quanto boilerplate há ou se é preciso criar um sistema de eventos por conta própria, se vários frameworks estão usando signals, pode haver um motivo; mesmo que leve tempo, vale tentar padronizar
    • Concordo. Ainda assim, a documentação de signals do Preact dá um contexto muito melhor
      https://preactjs.com/guide/v10/signals
      No Preact, quando um signal desce pela árvore via props ou context, apenas a referência do signal é passada, e como o componente observa o signal, não o valor, atualizar o signal pode não renderizar o componente novamente. Na prática, é possível ir diretamente ao componente da árvore que acessa .value
      Além disso, um signal rastreia quando o valor é acessado e quando é atualizado; no Preact, se você acessa .value de um signal dentro de um componente, esse componente é automaticamente renderizado de novo quando o valor daquele signal muda
    • Como JavaScript não tem reatividade embutida, adicionar reatividade inevitavelmente traz custo de abstração. É algo para usar quando necessário, não precisa ser a forma padrão de lidar com estado
      Pela minha experiência, a grande vantagem é poder modularizar estado reativo. No estilo imperativo, é preciso estado adicional para acompanhar mudanças, e a modularidade é alcançada por meio de abstração. Basta usar quando for necessário
      Criar um exemplo simples e ainda aplicável é uma questão de equilíbrio. Os casos em que a reatividade traz um benefício claro costumam ser mais complexos, por isso são mais difíceis de demonstrar do que exemplos simples, mas menos aplicáveis
    • A forma de explicar isso pode ser melhorada. Em exemplos pequenos, o problema não aparece muito; ele surge em escala maior. PRs são bem-vindos
    • Vale a pena evitar situações em que uma mudança de design se torna necessária ao ultrapassar determinado limiar de complexidade. A abordagem em JS puro tem um limite de escalabilidade em termos de complexidade do grafo de estado, e o verdadeiro problema não é a usabilidade antes ou depois do limiar, mas o fato de a usabilidade mudar de forma descontínua no momento em que esse limiar é ultrapassado
  • Quando Promises foram adicionadas ao JavaScript, eu tinha certa resistência, achando que seria preciso usar new Promise por toda parte
    Na prática, consigo contar nos dedos das duas mãos quantas vezes escrevi new Promise diretamente. Em vez disso, especialmente ao lidar com bibliotecas de terceiros, acabei usando .then com muito mais frequência
    No fim, o efeito cotidiano de adicionar Promise ao JavaScript foi fornecer uma interface bastante simples, em geral robusta e quase universal para vários comportamentos e recursos especiais oferecidos por bibliotecas de terceiros. Seja leitura de arquivos, requisições de API ou saída de etapas de build, ao usar .then(res => …) parece que você já está a meio caminho de algo funcional
    Se esta proposta de Signal cumprir um papel parecido em meio à explosão cambriana dos frameworks de UI reativos, sou a favor. Mais ainda, ela também pode ajudar a reatividade a se expandir para fora da UI. Muitas vezes imaginei árvores de estado de recálculo incremental para coisas que não são estado de UI

    • Eu via Promises principalmente como algo introduzido para viabilizar async/await, e foi isso que realmente melhorou muito a qualidade de vida. No trabalho, raramente é preciso escrever new Promise diretamente
      O .then das Promises iniciais foi uma grande melhoria em relação a delegates aninhados e funciona bem para cadeias simples, mas quando é preciso encadear Promises diferentes condicionalmente, ter tratamento de erro distinto para cada cadeia ou fazer retorno antecipado, o código pode ficar muito mais difícil de ler e manipular
      Com async/await, dá para escrever chamadas como se não fossem Promises, colocar try/catch facilmente em torno de uma chamada específica de Promise e fazer retornos antecipados de forma natural
  • Não entendo por que isso deveria se tornar parte da linguagem. Dá para fazer com bibliotecas, e já existem bibliotecas desse tipo. Como são pequenas, não é um grande fardo incluí-las no código, e adicionar algo à linguagem não deveria ser um objetivo em si
    É arrogante achar que as bibliotecas de UI em JS de hoje projetaram signals tão bem que eles deveriam virar parte da linguagem. Há muitas implementações de signals com diferentes trade-offs, e nenhuma delas merece um lugar especial na especificação do JavaScript
    Antes de essas bibliotecas usarem signals, elas usavam DOM virtual. Felizmente o DOM virtual não virou parte do JS; o que há de diferente em signals? Nada. O argumento para padronizá-los é ainda mais fraco do que era no caso do DOM virtual
    Vamos enfiar tudo que estiver na moda em um runtime no qual, na prática, não há como remover recursos que não queremos mais sem quebrar a web? É bem míope

    • Há um ponto correto aí. Não queremos a coisa errada, mas queremos a coisa certa
      UI reativa venceu. Mesmo em aplicações pequenas, quando se faz gerenciamento de estado, a complexidade explode, e esse é o ponto central que torna difícil usar JS puro. Para mim, qualquer framework reativo é melhor do que JS puro; sendo assim, talvez esteja faltando algum componente
      Já se passaram cerca de 10 anos, então é hora de pensar em um ponto de corte que possa ser padronizado. Se for feito direito, como Promise, pode reduzir a complexidade de casos de uso muito comuns
      Uma avaliação melhor é perguntar: “os frameworks reativos existentes usariam esta proposta?”. Se não, é preciso ver por quê, o que está faltando, o que é desnecessário e o que podemos aprender com UI e reatividade em outras linguagens. Vale a pena refinar essas experiências dispersas
    • Um bom motivo para padronizar Signals é que depuração parece um pesadelo. Imagine uma árvore profunda de signals computados disparando uns aos outros em cadeia, e você tendo que encontrar o ponto inicial dessa reação em cadeia. Se houver padronização, ferramentas de desenvolvedor poderão ser construídas em torno disso
    • Dá para dizer o mesmo sobre a maior parte de uma biblioteca padrão. Ainda assim, como diz a motivação, há um movimento para expandir a biblioteca padrão relativamente pequena do JS, de modo que não seja preciso importar um pacote para cada tarefa comum
      Dá para discutir essa necessidade, mas, se a biblioteca padrão vai ser expandida, olhar para o que é popular me parece uma boa abordagem
      signals não são um substituto do DOM virtual
    • Isso me lembra a proposta de Observable
  • Quando é preciso sinalizar algo para a aplicação inteira, usa-se eventos
    window.dispatchEvent(new Event('counterChange'));
    E qualquer parte da aplicação que quiser reagir pode se inscrever assim
    window.addEventListener('counterChange', () => { ... do something ... });
    Qual é o problema dessa abordagem?

    • Historicamente, esse exemplo é justamente o motivo pelo qual a web evoluiu para jQuery e, a partir daí, se dividiu nos mundos de Angular e React
      Tratamento de eventos fica bagunçado com muita facilidade. Para ver mais a fundo, basta olhar para bubbling e propagação de eventos
      Aplicações grandes precisam de tratamento de eventos robusto, e essa é uma das vantagens hoje menos evidentes de frameworks como Angular e Vue
      Você provavelmente não vai querer usar diretamente a API padrão de tratamento de eventos sem um framework. Ao lidar com adicionar, excluir, duplicar, disparar, remover, disparar uma única vez etc. para muitos elementos, podem surgir efeitos colaterais indesejados sérios
    • Segundo o texto, publicadores de eventos/observables causam trabalho desnecessário quando são chamados várias vezes
      A diferença em relação a signals é que o valor resultante só é calculado quando o consumidor final lê o valor. O momento em que se escreve de fato no signal é separado do agendamento de atualizações assíncronas de renderização, e a cadeia de cálculos executada pelo observador roda apenas uma vez durante a renderização
      Os valores intermediários enviados pelo signal desaparecem, então é difícil fazer muita coisa interessante dentro deles; na prática, é mais uma camada de abstração avançada para coordenar o ciclo de renderização
    • Signals também são, no fim, publicação/assinatura, mas a API é mais agradável de usar, porque listeners são adicionados e removidos automaticamente
      O desempenho também pode ser melhor. Por exemplo, se houver um cálculo que depende de dois valores, em result = a ? b : 0, se a for falso, não é preciso recalcular quando b mudar. Com signals isso acontece automaticamente, mas com publicação/assinatura tradicional seria necessário bastante código
    • Usei esse padrão por mais de 10 anos. A dificuldade é que, com o tempo, um listener pode disparar outro evento, e outro evento pode voltar à primeira rotina, criando um loop de listeners sem fim
      Também é difícil garantir que todos os listeners não criem esse tipo de disparo em cadeia
    • Essa abordagem tem todos os pontos fracos da arquitetura de publicação/assinatura destacados na proposta
  • Durante décadas, as pessoas vêm tentando entender por que rastreamento de estado e atualizações do DOM são tão difíceis
    Claro que é preciso um pouco de disciplina, mas isso parece muito mais simples do que as soluções que surgem a cada poucos anos. Backbone, Knockout, Angular, React, modificar a própria linguagem etc.; talvez meu modo de pensar seja fundamentalmente diferente
    Isso aparece até no nome das funções. Chamar a atualização de innerText de “render” não é realmente renderizar. No máximo, quem renderiza é o navegador, e o mesmo vale para todo o resto relacionado a pintura. Parece uma tentativa desesperada de complicar uma das funções mais simples do DOM, o que é realmente desconcertante

    • Em aplicações simples, é fácil
      Quando fica mais complexo, deixa de ser fácil
    • Na frase “é preciso um pouco de disciplina”, tenho a forte impressão de que, antigamente, eu teria sido um programador ASM irritado com aqueles programadores C portáveis malucos, e depois um programador C irritado com aqueles programadores Java malucos de segurança de memória
      O progresso da programação pode ser visto como um processo de eliminar a disciplina ritualística e rígida necessária para obter bons resultados
      Não quer dizer que React seja a próxima evolução, mas signals certamente são um passo na direção certa
    • Décadas é muito tempo. Dá para lembrar como as atualizações do DOM eram complicadas em cada navegador
      Sincronizar o DOM com o estado dos dados em si não é tão difícil, mas fazer isso com altíssimo desempenho a 60fps é tremendamente difícil. Especialmente ao criar uma API que não vaze e, ao mesmo tempo, não seja trabalhosa demais
      Sinceramente, pode ser até mais fácil desenhar pixels em um canvas, como em um jogo, do que transformar mudanças e refleti-las em uma árvore DOM viva
    • Estou criando uma aplicação de trading de FX com centenas de milhares de linhas. É do nível de substituir um aplicativo desktop pesado, com 20 a 30 desenvolvedores e vários clientes, cada um com seus próprios desenvolvedores. Se alguém tentar fazer isso sem framework, só posso desejar boa sorte
    • Penso exatamente igual. Já desenvolvi SPAs muito complexas e cheias de interações, mas ainda nunca encontrei o problema que essas coisas dizem resolver
  • Promises são um bom caso de sucesso, mas, sem async/await, não precisariam necessariamente ter sido padronizadas
    O rascunho atual diz se basear em designs de autores/mantenedores de Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz etc., e fico curioso para saber como os autores das bibliotecas existentes veem essa proposta. Também é interessante que React não esteja na lista
    Signals são um pouco parecidos com channels, mas diferem por serem broadcast em vez de ter um único receptor. Seria legal se isso permitisse que web workers se comunicassem por channels em vez de callbacks onMessage. Especialmente se, como em Go, fosse possível fazer select sobre signals/channels/promises, haveria uma vantagem sintática em relação a gerenciar vários mecanismos de mensageria concorrentes com callbacks. Por exemplo, permitir incluir signals em Promise.any

    • Discordo fortemente de que “sem async/await não haveria necessidade de padronizar”
      x instanceof Promise simplesmente não funciona. Se o método then da minha biblioteca aceita um callback de catch e o de outra biblioteca não, elas silenciosamente não interoperam, e não há como detectar isso. Quando finally é executado? Que expectativa se pode ter sobre quão assincronamente os callbacks serão executados?
      Sem um padrão, toda biblioteca que usa Promise precisa trazer seu próprio polyfill, porque não dá para confiar no que já existe. E também não dá para consumir de fato Promises de outras bibliotecas, porque não dá para acreditar que elas se comportem da forma esperada
      Isso não é especulação: foi exatamente assim durante anos, e foi um inferno que muita gente teve de suportar
    • Também há benefícios de padronização que não dependem de async/await. Os engines JavaScript puderam fazer otimizações de desempenho que beneficiam aplicações que usam muito Promise, e isso não teria sido possível se não fosse um padrão
    • O motivo de React não estar na lista é que signals, ao contrário de Preact, não fazem parte da API central do React
      Minha vaga intuição é que signals se parecem demais com um useEffect() generalizado e, se entrassem no React, tornariam ainda mais confuso o que acontece durante o ciclo de renderização. Para o bem ou para o mal, React escolheu uma forma de atualização diferente de signals. Ainda assim, posso estar errado sobre a aplicabilidade
    • O motivo de React não estar nessa lista é que seus efeitos são declarativos, não imperativos. Mudanças de props e re-renderização também podem ser vistas como declarativas, em um nível de abstração acima. useEffect isola de forma limpa o comportamento imperativo
      Isso parece muito com data binding do Ember e pode acabar virando um pesadelo imperativo. O estado padrão se aproxima de uma “arma apontada para o próprio pé”, e evitar que isso aconteça exige uma enorme carga cognitiva e metapadrões
    • Talvez dê para chamar isso de “EventEmitter”
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • Não entendi o exemplo no README linkado
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    Que biblioteca? Que framework? Foi aí que me perdi. O que é effect?
    effect(() => element.innerText = parity.get());
    Como effect sabe que deve chamar esse lambda sempre que parity mudar? Ele chama esse lambda toda vez que qualquer signal muda? Se for isso, por que falam de cache? Provavelmente não é isso
    De qualquer forma, se entendi corretamente o que os autores queriam transmitir, a ideia de signals em si parece válida. Mas o grande problema desse tipo de arquitetura desacoplada é que, quando a aplicação fica complexa o suficiente, você se perde tentando rastrear por que um evento específico ocorreu. Idealmente, signals deveriam ajustar o stack trace, de modo que, quando um callback fosse chamado, ele já incluísse o stack trace do código que disparou o signal em primeiro lugar

    • Há várias bibliotecas que exportam uma função chamada effect, permitindo executar código arbitrário em resposta a atualizações de signals. A introdução a signals e effects na documentação do Preact é boa: https://preactjs.com/guide/v10/signals#effectfn
      Pelo que entendi, esse tipo de função effect primeiro executa o callback uma vez para verificar quais signals foram acessados durante a execução, e chama o callback novamente sempre que um signal do qual ele depende é atualizado. Se o acesso ao signal for síncrono e single-threaded, o simples fato de um signal ter sido acessado durante a execução do callback basta para saber que esse callback deve assinar esse signal
      Também dá para fazer isso com getters. A função effect rastreia quais propriedades de um signal foram acessadas no método getter, e, pelo que sei, o Vue 2 usava esse tipo de abordagem no passado. Também é possível rastrear acessos a objetos com proxies. O exemplo da proposta tem um método get que é chamado para acessar o valor do signal, e a execução desse método permite rastrear as dependências
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • A chamada parity.get() registra uma dependência para a função passada a effect(). Quando parity é atualizado, essa função é chamada
      Ela não é chamada toda vez que um signal muda, mas apenas quando muda um signal do qual ela depende
      Nesse caso, parity depende de isEven, e isEven depende de counter. Então, quando counter é atualizado, toda a cadeia de dependências é invalidada; parity é invalidado e o callback é executado novamente
    • Implementações de signals, seja qual for o nome, em geral criam um grafo dinâmico de dependências, e as arestas surgem quando os nós são lidos. Em um contexto de rastreamento como esse effect hipotético, uma leitura cria uma aresta entre o nó de estado do signal e o nó de computação do effect, fazendo na prática com que este último assine escritas futuras do primeiro, para decidir quando reexecutar a computação
    • effect é uma função arbitrária que você quer chamar
      Em signals, o mecanismo de rastreamento de dependências sabe quais valores precisam ser recalculados e, como resultado, o sistema também passa a saber quais funções deve chamar novamente
    • Acho que um watcher seria necessário para implementar effect
  • Relacionado a isso, existe o S.js: https://github.com/adamhaile/s
    Gosto de signals. Ao criar UI, prefiro isso a qualquer outro primitivo, talvez com exceção do algoritmo de restrições Cassowary. Tento imitar signals em todas as linguagens que uso por diversão
    Mas não acho de jeito nenhum que isso deva entrar na própria linguagem JavaScript. Gostaria que deixassem a linguagem em paz por um tempo. As pessoas já têm dificuldade de acompanhar, e o TC-39 já está assustando as pessoas e afastando-as da linguagem

  • Isto parece muito com meu sistema de effects favorito em JS, o MobX
    A versão do MobX seria assim
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX é signals, sim. Só que são signals que rastreiam dependências implicitamente por meio de objetos proxy, em vez de rastreá-las explicitamente com getters
  • Passa a sensação de “vamos embutir na biblioteca padrão o framework que estou usando hoje em dia!”
    É parecido com tatuar o nome da namorada no corpo

    • Não é isso
      É colocar na biblioteca padrão um componente básico para o qual a maioria dos frameworks convergiu
      Promises também entraram na biblioteca padrão depois de serem amplamente usadas, e isto é parecido