本文我想和大家分享一下我对当前 Reactivity
方法和现状的理解。我并不是说我的观点就是对的,但我认为,正是通过分享自己的观点,我们才能对行业中的事物达成共识,我希望这些来之不易的见解能够对其他人有所帮助,并补充他们理解中缺失的部分。
reacitve 三剑客
我认为到目前为止,我们在行业中看到的 reacitive
方法有三种:
- 基于 value:也就是脏检查,应用的框架有 Angular, React, Svelte;
- 基于 observable : 应用的框架有 Angular with RxJS, Svelte;
- 基于 singnal:应用的框架有 Angular with signals, Qwik, React with MobX, Solid, Vue
接下来我来谈谈这三种方法:
基于 value
基于 value 的系统依赖于将状态作为简单值存储在“不可观察”引用中。
当我 说“observable” 时,我并不是指的是像 RxJS 这样的可观察对象。我指的是“可观察”这个词的常用用法,比如知道它什么时候发生了变化。“不可观察”意味着当值发生变化时,没有办法及时知道具体的实例。下面我给出三个例子:
- React
function Counter() {
const [count, setCount] = useState(0)
return <button onClick={() => setCount(count + 1)}>{count}</button>
}
- Angular
import { Component } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<h1>Counter: {{ count }}</h1>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
count: number = 0;
increment() {
this.count++;
}
}
- Svelte
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<div>
<h1>Counter: {count}</h1>
<button on:click={increment}>Increment</button>
</div>
在上面的每种情况下,状态都作为一个值存储在变量中。但关键是它是一个不可观察的值,以一种不允许框架知道(观察)值何时变化的方式存储在 JavaScript 中。
由于该值的存储方式不允许框架观察到变化,因此每个框架都需要一种方法来检测这些值何时发生变化,并将组件标记为脏组件。
一旦标记为 dirty,就会重新运行组件,以便框架可以重新读取/重新创建值,从而检测哪些部分发生了更改,并将更改反映到 DOM。
脏检查是基于 value 的系统所能采用的唯一策略。它将最后一个已知值与当前值进行比较。
那怎么知道什么时候运行脏检查算法呢?通常不同的框架方式不同:
- Angular: 隐式依赖
zone.js
来检测状态何时可能发生了变化。(因为它依赖于通过zone.js
的隐式检测,所以运行变更检测的频率比严格必要的要高。) - React: 显式依赖于开发人员调用
setState()
。 - Svelte: 自动生成 setState() 调用。
基于 Observable
Observable 对象是随时间变化的值。Observable 对象允许框架在值发生变化时及时知道具体的实例,因为将新值推送到 Observable 对象中需要特定的 API 来充当保护。
可观察对象是解决细颗粒 Reacitive 问题的明显方法。但是,因为 observable
需要显式调用 .subscribe()
和相应的调用 .unsubscribe()
,导致开发体验不好 。可观察对象也不能保证同步无故障的交付,UI 倾向于同步更新。下面我们给出代码示例:
- Angular
import { Component } from '@angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-counter',
template: `
<h1>Counter: {{ count$ | async }}</h1>
<button (click)="increment()">Increment</button>
`,
})
export class CounterComponent {
private countSubject = new BehaviorSubject<number>(0);
count$: Observable<number> = this.countSubject.asObservable();
increment() {
this.countSubject.next(this.countSubject.value + 1);
}
}
- Svelte
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() {
// Update the count by incrementing it
count.update(n => n + 1);
}
</script>
<div>
<h1>Counter: {$count}</h1>
<button on:click={increment}>Increment</button>
</div>
Svelte: 有趣的是,它有两个具有不同心智模型和语法的 Reacitive 系统。这是因为基于value 的模型只在
.svelte
文件中工作,所以将代码移出.svelte
文件需要一些其他的 Reacitive 原语(Stores)。
我相信每个框架都应该有一个可以处理所有用例的单一 Reacitive 模型,而不是基于用例的不同 Reacitive 系统的组合。
基于 Signal
Signal 就像可观察对象的同步表兄弟,没有订阅/取消订阅。我相信这是一个重大的编码改进,我也相信 Signal 是未来。
Signal 的实现并不明显,这就是为什么行业花了这么长时间才走到这一步。Signal 需要与底层框架紧密耦合,以获得最佳的编码体验和性能。
为了获得最好的结果,需要协调框架渲染和可观察对象更新。下面给出几个示例:
- Qwik
export const Counter = component$(() => {
const count = useSignal(123);
return (
<button onClick$={() => count.value++}>
{count.value}
</button>
);
});
- SolidJS
export const Counter = (() => {
const [count, setCount] = createSignal(123);
return (
<button onClick={() => setCount(count() + 1)}>
{count()}
</button>
);
});
- Vue
<template>
<section>
<h1>Count: {{ count }}</h1>
<button @click="incrementCount">+1</button>
</section>
</template>
<script setup>
import { ref } from 'vue';
const count = ref(0);
function incrementCount() {
count.value++;
}
</script>
Angular 正在研究 Signal ,但它们仍然需要 Signal 和模板的集成。
最后,总结一下我的观点。
可观察对象太复杂了,不太适合。因为只有 BehaviorSubject 可观察对象才能真正与 UI 一起工作。
在基于 Value 的系统中,性能又是极其消耗的。虽然值的变化不会破坏应用程序,只是当有一天你觉它太慢了的时候,并且当你想要进行优化它时,就会发现没有“明显”的东西需要修复。
对于基于 Signal 的系统,对于开发者,最初的理解门槛会稍微高一些,并且开发者很有可能从 Reacitive 悬崖上掉下来。因为如果你对 Signal 的反应错误,应用程序就会崩溃。但是解决问题的办法也会很明显。
其次,当一旦你开始优化基于 Value 的系统的时候,你就会开始接触到基于 Signal 的世界,在那里你可能会像处理 Signal 一样失去 Reacitive。本质上,基于 Value 的“优化”API是“低于标准的 Signal 的”。
这也是我喜欢 Signal 的第二个原因。Signal 开启了一种很酷的编码方式,它允许你可视化系统的响应式并调试它。
好啦,以上就是我的理解,希望对你有帮助!