Angular以及Qwik的作者MIŠKO HEVERY写了篇文章:useSignal()才是Web框架的未来(useSignal() is the Future of Web Frameworks),也引起一些讨论。包括Qwik在内的多个框架也已经实现了useSignal(),这些框架还有Vue、Preact、Solid以及Svelte等。

实际上signals并不是一个全新的概念,大约十年前的框架Knockout就曾经使用过。那为什么最近突然又重提此项技术?这主要得益于当今更加先进的编译技巧以及与jsx的深度集成,使signals的开发体验变得更好了。

什么是signal

如果了解过React的话,signal其实和useState非常类似,也是一种用来存储应用状态的方式。但是和useState存在一些区别:

useState是返回一个值以及一个set方法,而useSignal则是返回一个getter和一个setter,参见如下表示:

useState() => value + setter
useSignal() => getter + setter

这两种返回有什么区别吗?我们来讨论一下state的意义。

State的意义

state有两个独立的含义:

  1. 状态引用——对当前状态引用。
  2. 状态值——状态引用中存储的状态值。

举个例子,在React中,我们是这样定义state的:

export function Counter() {
  const [count, setCount] = useState(0);

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  );
}

在这个里面,useState返回的内容包括两项:

  1. count——状态的值
  2. setCount——状态的setter

我们再来看一看使用signal的例子:

export function Counter() {
  const [getCount, setCount] = createSignal(0);

  return (
    <button onClick={() => setCount(getCount() + 1)}>
      {getCount()}
    </button>
  );
}

createSignal返回的部分也包括两部分:

  1. getCount——状态的引用
  2. setCount——状态的setter

看起来似乎也没太大区别对不对?他们到底有什么用呢?

Signal与State

二者之间的最大区别在于:Signal可以通过getter去感知上下文,知道是谁在什么地方引用(订阅)了这个状态,从而可以在这个状态发生改变的时候直接通知这个对象(订阅者)。通过这个上下文感知,从而可以做到更细颗粒度的局部更新。而State返回的是一个状态值,所以State无法知道是组件的哪一部分使用了这个状态,因此在状态发生改变时必须重新渲染整个组件。

还是用上面的React的例子,当点击按钮时,调用setState方法更新状态,React并不知道这个页面的哪些部分引用了这个状态,因此会对整个Counter组件进行渲染。当这个组件变得复杂时,可以想象这个开销是非常大的。

我们再来看看在Qwik中使用useSignal的用法:

export function Counter() {
  const count = useSignal(0);

  return (
    <button onClick$={() => count.value++}>
      {count.value}
    </button>
  );
}

在这个例子中,.value属性代表的其实是一个getter和一个setter,只是语法不一样而已。在这个里面,由于使用了getter,signal知道了只有一个文本节点使用了count.value,于是当点击按钮时,只需要更新这个文本节点即可,而无需更新整个组件。

复杂一点的例子

一个稍微复杂一点的例子,包含两个计数器按钮以及两个展示组件:

export function Counter() {
  console. log('<Counter/>');
  const countA = useSignal (0);
  const countB = useSignal (0);
  return (
    <div>
      <button onclick$={() => countA.value++}>A</button>
      <button onClick$={() => countB.value++}>B</button>
      <Display count={countA.value} />
      <Display count={countB.value} />
    </div>
  );
}

export const Display = component$(
  ({ count }: { count: number }) => {
    console. log ('<Display count={${count}}/>');
    return <div>{count}!‹/div>;
  }
);

在这个例子中,只有需要更新的Display组件才会更新,另外一个无需更新的组件则不会重新渲染。

# 初始渲染输出
<Counter/>
<Display count={0}/>
<Display count={0}/>

# 单击时的渲染
(空白)

在React中,我们无法做到这样,当然我们可以用useMemo来优化渲染次数。

export default function Counter() {
  console. log('<Counter/>');
  const [countA, setCountA] = useState (0) ;
  const [countB, setCountB] = useState (0) ;
  return ( 
    <div>
      <button onClick={() => setCountA(countA + 1)]>A</button>
      <button onClick={() => setCountB(countB + 1)}>B</button>
      <MemoDisplay count={countA} />
      <MemoDisplay count={countB} />
    </div>
  );
}

export const MemoDisplay = memo (Display);
export function Display({ count }: { count: number }) {
  console. log(`<Display count={${count}}/>`);
  return <div>{count}!</div>;
}

即便优化之后,也会产生多次渲染:

# 初始渲染输出
<Counter/>
<Display count={0}/>
<Display count={0}/>

# 单击时的渲染
<Counter/>
<Display count={1}/>

因此,开发也会相对来说简单很多,不用花费更多的时间关心如何优化渲染,研究如何使用useMemo,PuerComponent等等。

总结

Signal是应用中存储状态的一种方式,与React的useState类似,两者的关键区别在于Signal返回一个getter和一个setter,而State只返回一个值和一个setter

Signal是响应式的,通过跟踪调用堆栈上下文实现相关订阅,达到局部更新的目的。

相比之下,React无法只能重新渲染整个组件树以响应状态变化。

另外,React团队成员对此的看法:

Rect可能引入类似的原语,虽然它性能很好,但是并不认为这是写UI代码的好方式,React的目标是希望通过编译器来达到同等性能。

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.