在JavaScript中为信号辩护
#javascript #网络开发人员 #solidjs #reactivity

在最近几周的所有对话中,我忘了谈论最重要的话题。你为什么要关心?我已经介绍了他们的evolutionopposition,但是我实际上还没有这样使用。

周围有一个与性能有关的叙述,这并非没有优点,但远不止于此。这也不仅仅是开发人员的经验。这是关于翻转当前范式的头部。

反应著名地普及:

view = fn(state)

这是一个非常有力的心理模型,用于思考UIS。但更重要的是,它代表了一个理想。要努力的事情。

现实更加混乱。基础的DOM是持久和可变的。幼稚的重新渲染不仅会昂贵,而且从根本上也打破了经验。 (输入失去焦点,动画等...)

有一些减轻这种情况的方法。我们建立了不断变化的结构,希望重塑我们的现实。但是在某个时候,我们需要将实施与理想分开,以便能够诚实地谈论这些事情。

所以今天我们要看信号,以及他们必须提供的信号。


与代码组织分离性能

Image description

这是您第一次意识到真正不同的事情正在发生的时刻。不过,还有更多。这不是使用全球状态乱扔应用程序的邀请,而是一种说明状态的方式独立于组件。

function Counter() {
  console.log("I log once");

  const [count, setCount] = createSignal(0);
  setInterval(() => setCount(count() + 1), 1000);

  return <div>{count()}</div>
}

同样,当计数器更新是一个可爱的技巧,但没有讲述整个故事时,它不会重新执行的console.log

事实是,这种行为持续在整个组件树中。起源于父组件并用于儿童的状态不会导致父母或子女重新运行。只有DOM的部分取决于它。道具钻探,上下文API或其他任何事情。

,不仅是关于在组件中传播状态变化的影响,而且是在同一组件中的多个状态。

function MoreRealisticComponent(props) {
  const [selected, setSelected] = createSignal(null);

  return (
    <div>
      <p>Selected {selected() ? selected().name : 'nothing'}</p>

      <ul>
        {props.items.map(item =>
          <li>
            <button onClick={() => setSelected(item)}>
              {item.name}
            </button>
          </li>
        )}
      </ul>
    </div>
  );
}

将示例从Svelte Creator Rich Harris的Virtual DOM is Pure Overhead到SolidJS移植示例很重要的是,使用信号更新所选状态除了更改<p>中的文本外,不会引起任何执行。没有重新运行列表或扩散。如果行更新中的一个名称,甚至是正确的。使用信号,我们可以直接更新该按钮的文本。

注意:在呈现循环时,使用<For>组件而不是.map是习惯的,以便在插入,删除或移动的items条目时不重新创建每一行。

您可能会在想,“我明白了。它很快。但是我从来没有难以确保在诸如反应之类的事情中表现良好。”但是要点不仅如此。

您不再需要关注最佳执行的组件。

您可以将整个应用程序放入一个组件或许多组件中,并获得相同的好处。为了自己的缘故,您可以分解组件。您想如何组织应用程序。不是因为您需要隔离更改UI的某些部分。如果表现成为一个问题,那将不是由于您的组件的结构需要昂贵的重构。

这对开发人员的体验并不重要。


将动态与静态分开

有一些对话表明这是一件坏事。如果您想要更多观点,请参见@dan_abramovresponse to my previous article

我不仅想谈论为什么这是一件好事,而且还要谈论这实际上是一件了不起的事情。能够优化每个都是有益的。这是与基础平台对齐的那些地方之一。

从经典上讲,使用分布式事件系统(如信号与运行自上而下)的分布式事件系统的权衡。虽然更新将更快地用于事件系统,但创建时,它具有设置订阅的其他开销。

这甚至是由于Web通常是面向文档的接口而加重的。即使在单页应用程序中,您也将进行很多涉及很多创建的导航。

是有道理的。但是,Web平台意识到了这一成本,并且使创建元素的效率比单独创建元素更有效。提取质量创造的静态零件会比这些订阅更大。

和浏览器的好处不会停止。使用基于信号的系统,您的代码量表的复杂性,大小和执行与UI的交互程度,而不是其中有多少元素。

考虑一个几乎没有交互的服务器渲染的页面。也许是一个电子商务网站。静态零件是服务器渲染的HTML。您甚至不需要代码才能使其交互。只需从捆绑包中删除静态零件即可。

builder.io的史蒂夫(1:16)解释了这在Qwik中的工作方式:

诚然,这主要是表演问题。它来自Islands architectureReact Server Components的动机。它解决了一个非常real pain point we are facing today,其趋势越来越多,JavaScript束和放慢的初始页面负载。

总的来说,我的立场是这种分离导致一定程度的透明度。它使解释和推理实际发生的事情变得更容易。虽然不如理想简单,但它使逃生舱口更加连贯,这是任何系统的重要组成部分。


通用UI的语言

关于信号的最有力的事情之一就是将其影响视为一种语言。我不是说汇编的语言。信号完全是运行时机制。这里没有魔术。只是一个定向的无环图。

似乎很明显,在状态,衍生状态和效果的概念中存在融合,并不是所有的心理模型和实现都列为一致。

信号独立于任何组件或渲染系统。仅代表国家关系。与诸如React Hooks之类的东西不同,它具有其他原始词来描述如何守护useCallbackReact.memo和诸如稳定参考(useRef)之类的概念以处理效果的协调。

文章底部列出的Dan的两篇文章都是对如何有效使用这些原语的非常好的探索。

此外,信号借给可追溯性。他们为您提供了一种理解什么更新和原因的方式。

他们鼓励导致更多声明代码的模式。通过围绕数据而不是组件流进行组织代码,我们可以看到哪些数据正在驱动变化。

// control flow
const count = videos.length;
let heading = emptyHeading;
let somethingElse = 42;
if (count > 0) {
  const noun = count > 1 ? 'Videos' : 'Video';
  heading = count + ' ' + noun;
  somethingElse = someOtherStuff();
}

// derived data
const format = (count) => count > 1 ? 'Videos' : 'Video';

const count = videos.length;
const heading = count > 0 ? format(count) : emptyHeading;
const somethingElse = count > 0 ? someOtherStuff : 42;

它提出了一个关于代码目的的有趣问题。我们是否应该优化它,以使其更容易写或更易于阅读?


好的,但是权衡呢?

Image description

肯定有权衡。最明显的是,它们使数据变得特别,而不是应用数据的应用。我们不再处理普通物体,而是针对原始物体。这与承诺或事件发射器非常相似。您是在推理数据流而不是控制流。

JavaScript不是数据流语言,因此可能会失去反应性。公平地说,在没有工具或编译的情况下,任何JavaScript UI库或框架都是如此。对于信号,这更加强调您访问该值很重要。

我称此信号为( singular )挂钩规则。有后果。有一个学习曲线。它推动您以某种方式编写代码。当使用代理之类的事物时,还有其他警告,例如JavaScript语言中的某些机制(例如传播,破坏)的使用限制。

另一个考虑因素是处置。订阅将两种方式链接起来,因此,如果一侧长期存在,则可以将内存保留的时间比所需的更长。现代框架非常擅长自动处理此处理,但这是信号设计所固有的。

最后,从历史上看,人们担心大型不可控制的图。周期和不可预测的繁殖。这些问题主要是由于past several years所做的工作。我会走这些问题,这是信号解决的问题,以及为什么您会通过其他消息/事件系统使用它们。


结论

有很多方法可以应对创建出色的用户界面的挑战。我的目的是将讨论扎根,但我认为这里有很多兴奋。

当您以原始基础的基础建造时,您可以做很多事情。减少JavaScript负载开销和增量交互性的探索是一个自然适合的区域。

是使用信号来巨大的好处,您不需要编译器。甚至没有模板。您可以使用标记的模板文字,并在没有构建步骤的情况下完成所有操作。尽管我们倾向于使用汇编使人体工程学更顺畅。但是它们也是编译的绝佳选择。

编译器和语言探索会在您可以定位的有效构建块时变得容易得多。这不仅是我们的事,而且对AIS。我们已经看到这一点建议改进从使用分析到驱动代码拆分以优化初始负载到优化编译器的能力来理解代码意图的一切。

是最适合开发人员持有的信号还是是机器的低级原始词,它们似乎是Web前端不断发展的世界中的重要一步。


相关资源:

A Hands-on Introduction to Fine-grained Reactivity
The Evolution of Signals in JavaScript
React vs Signals: 10 Years Later
Rich Harris的Virtual DOM is Pure Overhead
Components are Pure Overhead
Rich Harris的Metaphysics and JavaScript
Making setInterval Declarative with React Hooks和Abramov
Before you memo() by dan Abramov