useSignal() 是 Web 框架的未来

625 阅读10分钟

最近很火的一篇文章 useSignal() is the Future of Web Frameworks,甚至让 尤大专门为 Vue3 文档添加一个小节—— api-design-trade-offs

看了不少人翻译,但是都没给出参考链接原文链接在这:www.builder.io/blog/usesig… 下面是中文对比译文。


2023年2月16日,由 MIŠKO HEVERY@QwikDev@angular@angularjs作者) 撰写。

image.png

A signal is a way to store the state of your application, similar to useState() in React. But there are some key differences that give Signals the edge.

Signal 是一种存储应用程序状态的方式,类似于 React 中的 useState() 。但是,有一些关键差异使 Signals 具有优势。

What Signal is ,什么是 Signal

an image of the difference between useState and useSignal. useState ⇒ value + setter, useSignal ⇒ getter + setter

The key difference between Signals and State is that Signals return a getter and a setter, whereas non-reactive systems return a value (and a setter). Note: some reactive systems return a getter/setter together, and some as two separate references, but the idea is the same.

Signals 和 State 之间的主要区别在于 Signals 返回一个 getter 和一个 setter,而非响应式系统的 State 往往返回一个值(和一个 setter)。注意:有些响应式系统一起返回一个 getter/setter(Vue3 的 ref),有些则返回两个单独的引用(SolidJS 的 createSingal),但思想是一样的。

State vs. State, 不同的 State

The issue is that the word State conflates two separate concepts.
问题是 State 这个词混淆了两个独立的概念。

  • StateReference:  The state-reference is a reference to the state.
    状态引用:state-reference 是对状态的引用。
  • StateValue:  This is the actual value stored in state reference/storage.
    状态值:这是存储在状态引用/存储中的实际值。

Why is returning a getter better than returning a value? Because by returning the getter, you can separate the passing of the state-reference from a reading of the state-value.

为什么返回一个 getter 比返回一个值更好?因为通过返回 getter,您可以将状态引用的传递与状态值的读取分开。

Let’s look at this SolidJS code as an example.
让我们以 SolidJS 代码为例。

Screenshot of a code example of a counter component in Solid.js.

  • createSignal(): allocates the StateStorage and initializes it to 0.
    createSignal() :分配 StateStorage 并将其初始化为 0 。
  • getCount: a reference to the store that you can pass around.
    getCount :可以通过传递存储值的引用。
  • getCount(): says retrieve the state value.
    getCount() :表示检索状态值。

I don’t get it! Looks the same to me, 我不明白!这对我来讲没啥区别

The above explains how Signals are different from the good old state but does not explain why we should care.

上面解释了 Signals 与良好的旧状态有何不同,但没有解释为什么我们应该关心。

Signals are reactive! This means that they need to keep track of who is interested in the state (subscriptions) and, if the state changes, notify the subscribers of the state change.

Signals 是响应式的!这意味着他们需要跟踪谁对状态(订阅)感兴趣,并且如果状态发生变化,则通知订阅者状态变化。

To be reactive, Signals must collect who is interested in the Signal’s value. They gain this information by observing in what context the state-getter is invoked. By retrieving the value from the getter, you are telling the signal that this location is interested in the value. If the value changes, this location needs to be re-evaluated. In other words, invoking the getter creates a subscription.

为了具有响应式特性,Signals 必须收集谁对 Signals 值感兴趣。通过观察在什么情况下调用状态的 getter 来获取此信息。通过从 getter 中检索值,您告诉 Singal 该位置对该值感兴趣。如果值发生变化,则需要重新执行此位置。换句话说,调用 getter 创建一个订阅。

This is why passing the state-getter rather than the state-value is important. The passing of state-value does not give the signal any information about where the value is actually used. This is why distinguishing between state-reference and state-value is so important in signals.

这就是为什么传递状态 getter 而不是状态值很重要的原因。 state-value 的传递不会向 signal 提供有关实际使用该值的位置的任何信息。这就是为什么区分状态参考和状态值在信号中如此重要。

For comparison, here is the same example in Qwik. Notice that (getter/setter) has been replaced with a single object with a .value property (which represents the getter/setter). While the syntax is different, the inner workings remain the same.

为了进行比较,这里是 Qwik 中的相同示例。请注意,(getter/setter) 已替换为具有 .value 属性(表示 getter/setter)的单个对象。虽然语法不同,但内部工作原理保持不变。

Screenshot of a code example of a counter component in Qwik.

Importantly, when the button is clicked and the value is incremented, the framework only needs to update the text node from 0 to 1. It can do that because, during the initial rendering of the template, the Signal has learned the count.value has been accessed by the text node only. Therefore it knows that if the value of the count changes, it only needs to update the text node and nothing else.

重要的是,当单击按钮并增加值时,框架只需要将文本节点从 0 更新为 1 。它之所以能够这样做,是因为在模板的初始渲染期间,Signal 了解到 count.value 仅由文本节点访问。因此它知道,如果 count 的值发生变化,它只需要更新文本节点,不需要更新其他任何内容。

Shortcomings of useState(),  useState() 的缺点

Let’s look at how React uses useState() and its shortcomings.

让我们看看 React 如何使用 useState() 以及它的缺点。

Screenshot of a code example of a counter component in React.

React useState() returns a state-value. This means that useState() has no idea how the state-value is used inside the component or the application. The implication is that once you notify React of state change through a call to setCount(), React has no idea which part of the page has changed and therefore must re-render the whole component. This is computationally expensive.

React useState() 返回一个状态值。这意味着 useState() 不知道组件或应用程序内部如何使用状态值。这意味着一旦您通过调用 setCount() 通知 React 状态发生变化,React 就不知道页面的哪一部分发生了变化,因此必须重新渲染整个组件。这在计算上是昂贵的。

useRef() does not render ,useRef() 不渲染

React has useRef(), which is similar to useSignal(), but it does not cause the UI to re-render. This example looks very similar to useSignal() but it will not work.

React 有 useRef() ,类似于 useSignal() ,但它不会导致 UI 重新渲染。此示例看起来与 useSignal() 非常相似,但它不会起作用。

Screenshot of a code example of a counter component with useRef in React.

useRef() is used exactly like a useSignal() to pass a reference to the state rather than the state itself. What useRef() lacks are subscription tracking and notifications.

useRef() 的使用与 useSignal() 完全一样,用于传递对状态的引用而不是状态本身。 useRef() 缺少的是订阅跟踪和通知。

The nice thing is that in signal-based frameworks, useSignal() and useRef() are the same thing. useSignal() can do what useRef() does plus subscription tracking. This further simplifies the API surface of the framework.

可喜的是在基于 Signal 的框架中, useSignal() 和 useRef() 是同一件事。 useSignal() 可以做 useRef() 做的事,再加上订阅跟踪。这进一步简化了框架的 API 表面。

useMemo() built-in  内置useMemo() 

Signals rarely require memoization because they do the least amount of work out of the box.

Singals 不需要开启记忆缓存,因为它们开箱即用的工作量最少。

Consider this example of two counters and two children components.
考虑这个包含两个计数器和两个子组件的示例。

Screenshot of a code example of a counter component with a child Display component in Qwik.

In the above example, only the text node of one of the two Display components will be updated. The text node that doesn't get updated will never print after the initial render.

在上面的示例中,只有两个 Display 组件之一的文本节点将被更新。未更新的文本节点将永远不会在初始渲染后打印。

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
(blank)

You actually can’t achieve the same in React because, at the very least, at least one component needs to re-render. So let’s look at how to memoize components in React to minimize the amount of re-rendering.

实际上,你无法在 React 中实现相同的效果,因为至少有一个组件需要重新渲染。那么让我们看看如何在 React 中记忆组件以最小化重新渲染的数量。

Screenshot of a code example of a counter component with a child Display component with the memo function wrapping it in React.

But even with memoization, React will rerun the re-render much more.
但即使有 memo,React 也会重新运行重新渲染更多。

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
<Counter/>
<Display count={1}/>

Without memoization, we would see:
如果没有 memo,我们会看到:

# Initial render output
<Counter/>
<Display count={0}/>
<Display count={0}/>

# Subsequent render on click
<Counter/>
<Display count={1}/>
<Display count={0}/>

That is a lot more work than what Signals have to do. So, this is why signals work as if you memoized everything without actually having to memoize anything yourself.

这比 Signals 必须做的工作多得多。所以,这就是为什么 Signal 就像你记住了所有东西而不需要自己真正记住任何东西一样工作的原因。

Prop drilling 属性穿透

Let’s take a common example of implementing a shopping cart.
让我们举一个实现购物车的常见例子。

a screenshot of three components in React that make up the code for a shopping cart.

The state of the cart is usually pulled up to the highest common parent between the buy button and where the cart is rendered. Because the buy button and the cart are far apart in the DOM, this often is very close to the top of the component render tree. In our case, we call it the common ancestor component.

购物车的状态通常会向上拉到购买按钮和呈现购物车的位置之间的最高公共父级。因为购买按钮和购物车在 DOM 中相距很远,所以它通常非常靠近组件渲染树的顶部。在我们的例子中,我们称它为公共祖先组件。

The common ancestor component has two branches:
公共祖先组件有两个分支:

  1. One which drills the setCart functions through many layers of components until it reaches the buy button.
    一个通过多层组件钻取 setCart 函数,直到它到达购买按钮。
  2. The other drills the cart state through many layers of components until it reaches the component which renders the cart.
    另一个通过多层组件钻取 cart 状态,直到它到达呈现购物车的组件。

The problem is that every time you click the buy button, most of the component tree has to rerender. This leads to an output similar to this:
问题是每次点击购买按钮时,大部分组件树都必须重新渲染。这会导致类似于此的输出:

# "buy" button clicked
<App/>
<Main/>
<Product/>
<NavBar/>
<Cart/>

If you do use memoization then you can avoid the setCart prop-drilling branch but not the cart prop-drilling branch, so the output would still look like so:

如果你确实使用了 memo,那么你可以避免让 setCart被当做属性传递,但不能避免cart 当做属性被传递,所以输出仍然是这样的:

# "buy" button clicked
<App/>
<NavBar/>
<Cart/>

With signals, the output is like so:
对于信号,输出如下所示:

# "buy" button clicked
<Cart/>

This greatly reduces the amount of code that needs to execute.
这大大减少了需要执行的代码量。

Which frameworks support it? ,哪些框架支持它?

An image with 4 logos of frameworks that use signals.

Some of the more popular frameworks which support signals are VuePreactSolid, and Qwik.
一些支持 Signal 的流行的框架是 Vue、Preact、Solid 和 Qwik。

Now, signals are not new; they have existed in Knockout and probably other frameworks before then. What is different is that signals have greatly improved their DX in recent years through clever compiler tricks and deep integration with JSX, which makes them very succinct and a pleasure to use — and that part is genuinely new.

现在,Signal 不是新的;在此之前,它们已经存在于 Knockout 和其他框架中。不同之处在于,Signal 通过巧妙的编译器技巧和与 JSX 的深度集成,极大地改进了它们的 DX,这使得它们非常简洁并且使用起来很愉快——而那部分确实是真正的新事物。

Conclusion 结论

A signal is a way to store state in an application, similar to useState() in React. However, the key difference is that signals return a getter and a setter, whereas non-reactive systems return only a value and a setter.

Signal 是在应用程序中存储状态的一种方式,类似于 React 中的 useState() 。然而,关键区别在于信号返回一个 getter 和一个 setter,而非响应式系统只返回一个值和一个 setter。

It is important because signals are reactive, meaning they need to keep track of who is interested in the state and notify subscribers of state changes. This is achieved by observing the context in which the state-getter is invoked, which creates a subscription.

这很重要,因为信号是响应式的,这意味着它们需要跟踪谁对状态感兴趣并通知订阅者状态更改。这是通过观察调用状态获取器的上下文来实现的,它创建了一个订阅。

In contrast, useState() in React returns only the state-value, meaning it has no idea how the state-value is used and must re-render the whole component tree in response to state changes.

相比之下,React 中的 useState() 只返回状态值,这意味着它不知道如何使用状态值,必须重新渲染整个组件树以响应状态变化。

In recent years signals have reached a DX which makes them no harder to use than traditional systems. For this reason, I think the next framework you will use will be reactive and based on signals.

近年来,Signals 已经到达 DX,这使得它们使用起来并不难。出于这个原因,我认为您将使用的下一个框架将是响应式的并且基于 Signals。