React vs Signals: 10 Years Later - Dan Abramov 舌战群儒

161 阅读9分钟

本文是关于React 与 Signals的讨论中 Dan Abramov 的一些观点和看法,原文

I'm not sure I understood this article. I think we're talking about different things. The responses you wrote don't correspond to what I meant to say. I'll try to clarify.

我不确定我理解了这篇文章。我认为我们在谈论不同的问题。你写的回答与我想要表达的不符。我会尝试澄清。

The problem that React "fixes" is inconsistent UI between initialization and updates. Before React, you would typically write some logic for initialization of some UI, and write a separate piece of logic that would update that UI to match the latest state. Like you did with the imperative manual button example. Of course, the problem with this is that they get out of sync.

React “fixes” 是解决初始化和更新之间的不一致UI的问题。在React之前,通常您会编写一些逻辑来初始化一些UI,并编写一个单独的逻辑来更新该UI。就像你在命令式手动按钮示例中所做的那样。当然,问题在于它们会失去同步。

The key insight is that the user doesn't care if the component has just appeared or if it updated. The end result is supposed to be the same. So we want to somehow get to the "end result" based on the current information. For React, this means re-running your component function and figuring out how to update the UI to match that. For Solid, this means re-running the things embedded in your template.

关键是用户不关心组件是刚出现还是有更新。最终结果应该是相同的。因此我们希望基于当前信息以某种方式达到“最终结果”。对于React,这意味着重新运行您的组件函数并找出如何更新UI以匹配它。对于Solid,这意味着重新运行并嵌入在您的模板中。

So far so good—but not quite.

目前为止还不错,但还不够?

The interesting question to me is where do you place the rendering logic. The kind of logic that governs what your component displays. Your example omits any realistic logic, so let's consider this React component:

对我来说有趣的问题是,你会把渲染逻辑放在哪里。渲染逻辑是指控制组件显示内容的逻辑。你的示例省略了任何现实逻辑,所以让我们考虑一下这个React组件:

// Working version in React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
  }
  return <h1>{heading}</h1>
}

The big idea in React is that you should be able to write code like this.

React的重要思想是,你应该能够像这样编写代码

You should be able to write rendering logic that takes some data, makes some decisions, formats some inputs, and then embeds them into the template. And that logic should not describe initialization only — it should run on updates too!

您应该能够编写渲染逻辑,该逻辑需要一些数据,做出一些决策,格式化一些输入,然后将它们嵌入到模板中。而且这个逻辑不仅仅应该描述初始化过程,还应该在更新时运行!

The user doesn't care whether it's initialization or update. The user just wants to see the latest state — whatever it is — reflected on the screen. That's what React "fixed": it made rendering logic reactive by default. Whenever you write rendering logic in React, you can be confident that it won't get out of sync with the data you pass.

用户并不关心是初始化还是更新操作。用户只想看到最新的状态——无论是什么——反映在屏幕上。这就是React所“修复”的问题:它默认情况下使渲染逻辑具有响应性。每当您在React中编写渲染逻辑时,您可以确信它不会与您传递的数据不同步。

Solid follows a different strategy. Only "holes" in the template are reactive. As a result, code like this doesn't work in Solid (even if I access stuff via props.something):

Solid采用了不同的策略。只有模板中的“空白”部分是具有响应性的。因此,即使我通过props.something访问内容,像这样的代码在Solid中也无法工作:

// Broken version in Solid
function VideoList(props) {
  const count = props.videos.length; // never updates in Solid 
  let heading = props.emptyHeading; // never updates in Solid
  if (count > 0) {
    const noun = count > 1 ? "Videos" : "Video";
    heading = count + " " + noun;
  }
  return <h1>{heading()}</h1>
}

Yes, I get the point, it's not supposed to work in Solid because things execute once. Since the linter warns about this code, it's fair to say that this is not a valid example for me to complain about.

是的,我明白了,这在Solid中不应该工作,因为它只会执行一次。由于linter会警告这段代码,所以可以说这不是一个有效的例子。

What is the canonical way to structure this code instead? From my understanding (I might be wrong), the idiomatic Solid code would be:

那么,代替这段代码的规范化的结构方式是什么?据我理解(可能有误),典型的 Solid 代码结构应该是:

// Working version in Solid
function VideoList(props) {
  const count = () => props.videos.length;
  const heading = () => {
    if (count() > 0) {
      const noun = count() > 1 ? "Videos" : "Video";
      return count() + " " + noun;
    } else {
      return emptyHeading;
    }
  }
  return <h1>{heading()}</h1>
}

This reads pretty nicely, but note that with Solid, I've had to restructure the code around each value instead of relying on the control flow of the outer function.

这段代码读起来很好,但是要注意的是在使用Solid后,我不得不针对每个值重新构建代码结构,而不是依赖外部函数的控制流程.

Suppose I wanted to add more rendering logic into count > 0 condition (for example, to determine some other variable). In React, I would put it inside the if statement I already have:

假设我想在count >0条件中添加更多的渲染逻辑(例如: 确定其他变量). 在React中,我会将它放在我已经有的if语句中:

// Working version in React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  let somethingElse = 42; // can add something here
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
    somethingElse = someOtherStuff(); // can add something here too
  }
  return (
    <>
      <h1>{heading}</h1>
      <h2>{somethingElse}</h2>
    </>
  );
}

In Solid, I don't know how to do this other than to duplicate the if statement again inside a new const somethingElse = () => { /* my logic here */ } binding:

在 Solid 中,除了在一个新的const somethingElse = () => { /* my logic here */ } 绑定中再次复制if语句之外,我不知道该怎么做

// Working version in Solid
function VideoList(props) {
  const count = () => props.videos.length;
  const heading = () => {
    if (count() > 0) { // can't add the logic here :(
      const noun = count() > 1 ? "Videos" : "Video";
      return count() + " " + noun;
    } else {
      return emptyHeading;
    }
  }
  const somethingElse = () => {
    if (count() > 0) { // let's put this here i guess
      return someOtherStuff();
    } else {
      return 42;
    }
  });
  return (
    <>
      <h1>{heading()}</h1>
      <h2>{somethingElse()}</h2>
    </>
  );
}

I get that there are ways to compress this specific example to be shorter, but my point is that you can't rely on top-level control flow for any of your rendering logic. You have to group things by individual values. (Or you could return multiple things from heading and rename it to model or something.) Another solution in Solid is using DSL like <When>. The downside of that is now you can't easily move the computation back into JS when you want to read its result inside an event handler — for example, to create similar branching logic.

我知道有一些方法可以缩短这个特定示例,但我的观点是你不能依赖顶层控制流来处理你的渲染逻辑。你必须按照单个值分组。 (或者你可以从heading返回多个值并将其重命名为model或类似名称。)在 Solid 中的另一个解决方案是使用像<When>这样的DSL。缺点是现在当你想要在事件处理程序内读取其结果时,你不能轻易地将计算移回JS中 ——例如,创建类似的分支逻辑。

That is the point I was trying to make.

 这就是我想表达的观点。

In Solid, only your template (and things explicitly referenced from it) re-executes. So putting rendering logic into the top-level component body is a mistake. You can't use top-level control flow or read values at the top level because that would undo the "fix": your initialization logic would diverge from your update logic. The linter flags it.

Solid中,只有您的模板(以及从模板中明确引用的内容)会重新执行。因此,将渲染逻辑放入顶级组件体中是一个错误。您不能在顶层控制流中使用或读取值,因为这会撤销“修复”:您的初始化逻辑将与您的更新逻辑分歧。这是由linter标记出来的.

In React,all your rendering logic is your "template". This lets you use if statements and control flow without regrouping your code around every value you render. This also ensures that the user always sees fresh values. That's what I meant by React not "missing" updates. React doesn't let you write rendering logic that leaves initialization and updates out of sync.

在React中,所有的渲染逻辑都是您的“模板”。这使您可以使用if语句和控制流,而无需在渲染每个值时重新组织代码.这也确保用户始终看到最新的值.,这就是我所说的React不会“错过(missing)”更新的意思。React不允许您编写渲染逻辑,使初始化和更新处于不同步的状态。

The benefit of the Solid approach is that you can avoid re-executing parts of that logic because you've structured the code around the values (rather than around the control flow). Similar to how you have to restructure your code around the values when you optimize it with useMemo. But for us, this isn't the desirable end state.

Solid 方法的好处是,您可以避免重新执行部分逻辑,因为您已经围绕值将代码结构化(而不是控制流)。就像当您使用useMemo优化代码时,需要围绕值重新组织代码一样。但对我们来说,这不是理想的最终状态。

With the compiler, the goal is to be able to write code without regrouping it:

使用编译器的目标是能够编写代码而无需重新组织它:

// Working version in React (again)
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  let somethingElse = 42;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
    somethingElse = someOtherStuff();
  }
  return (
    <>
      <h1>{heading}</h1>
      <h2>{somethingElse}</h2>
    </>
  );
}

And then the compiler would figure out the "groups" so that it has comparable performance to useMemo or something even more fine-grained. That's the hard part, but hopefully this helps explain the vision.

然后编译器会找出“组”以使其具有可与useMemo或更细粒度的东西相媲美的性能。这是困难的部分,但希望这有助于解释这个愿景。

Coming back to your final code example. Remove useMemo, and the React code still works. Would the Knockout example still work if you did not mark this as pureComputed()? This seems a bit similar to how Solid examples require you to wrap things.

回到你的最终代码示例。删除useMemo,React代码仍然可以正常工作。如果您没有将Knockout示例标记为pureComputed(),它是否仍然可以工作?这似乎有点类似于Solid示例需要您包装代码的方式。

The beauty of React is that making things "computed" is an optimization, not a requirement. An optimization we can eventually put under the hood. React does not require you to write rendering logic inside-out just to get things to update. In React, everything is reactive by default. That's what I was trying to convey — perhaps poorly.

React的美妙之处在于,“计算”只是一种优化,而不是必需;那这种优化我们最终可以放在底层。React 渲染更新逻辑不需要里外编写(即不需要你操作dom)。在React中,默认情况下,所有内容都是响应式的。这就是我试图传达的内容 -也许表达得不好。

Apologies if something is incorrect in my post. It's a bit hard to write these responses impromptu. But I figured I should since my tweets were pulled here. :) Appreciate the conversation.

如果我的帖子有错误,请谅解。即兴写这些回复确实有点困难。但是我认为我应该这样做,因为我的推文被引用了。:) 感谢交流。