2023 年了,我即将跑路的同事出去面试的时候,告诉我发现面试官还在问“不同框架的响应式有什么区别”这样老生常谈的问题!
正好最近看到 Qwik 的作者 Miško Hevery 分享了自己的一些见解,非常简洁清晰,学完了可以直接对付面试官了。
以下是我整理的原文:
我想分享一下我对当前响应式方法和格局的理解。以下是我个人的观点和意见,其中一些可能有些激进,所以做好准备。(我并不是说我的观点是正确的,但这就是我对这个世界的看法。)
我认为通过分享自己的观点,我们可以在行业中达成共识,我希望这些我多年来辛苦获得的见解对他人有所帮助,可以补充他们对问题的理解中的缺失部分。此外,我非常重视反馈,毕竟即使经过这么多年,我的理解也更像是一个精心编织的网络,而不是坚固的钢笼。
我认为迄今为止,在行业中有三种基本的响应式方法:
基于值的系统依赖于将状态存储在本地(非可观察)引用中,作为简单的值。
当我说“可观察”时,我并不是指像 RxJS 这样的 Observables。我指的是可观察这个词的常见用法,即知道何时发生变化。而“非可观察”意味着没有办法知道值在具体的时间点上发生了变化。
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
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++;
}
}
<script>
let count = 0;
function increment() {
count += 1;
}
</script>
<div>
<h1>Counter: {count}</h1>
<button on:click={increment}>Increment</button>
</div>
在上述每种情况下,状态以值的形式存储,可以是变量、封闭在变量中,或者是属性。但关键是它只是一个非可观察的值,以一种不允许框架在值发生变化时知道(观察)的方式存储在 JavaScript 中。
由于值是以一种不允许框架观察到的方式存储的,每个框架都需要一种方式来检测这些值的变化并将组件标记为"dirty"。
一旦标记为"dirty",组件会重新运行,以便框架可以重新读取/重新创建这些值,从而检测哪些部分发生了变化,并将变化反映到 DOM 中。
🌶️ 小抄:脏检查是值为基础的系统唯一可用的策略。将最新已知值与当前值进行比较。这就是方法。
你如何知道何时运行脏检查算法?
zone.js
来检测状态可能已发生变化的时机(由于依赖于zone.js
的隐式检测,它比严格所需的更频繁地运行变更检测)。setState()
。setState()
调用)。Observables 是随时间变化的值。Observables 允许框架知道值发生变化的具体时间点,因为将新值推送到 Observable 需要一个作为守卫的特定 API。
Observables 是解决细粒度响应式问题的明显方法,但是它们的开发体验不是最好的,因为 Observables 需要显式调用.subscribe()
和相应的.unsubscribe()
。Observables 也不能保证同步的无故障传递,这给偏向同步(事务性)更新的 UI 带来了问题。
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);
}
}
<script>
import { writable } from 'svelte/store';
const count = writable(0);
function increment() {
// 更新计数值
count.update(n => n + 1);
}
</script>
<div>
<h1>Counter: {$count}</h1>
<button on:click={increment}>Increment</button>
</div>
Svelte:有趣的是,它有两种具有不同思维模型和语法的响应式系统。这是因为基于值的模型只适用于
.svelte
文件,所以将代码移出.svelte
文件需要其他的响应式原语(Stores)。
我认为每个框架应该有一个单一的响应式模型,可以处理所有的用例,而不是基于用例的不同响应式系统的组合。
Signal 类似于 Observable 的同步版本,但没有 subscribe/unsubscribe。我认为这是开发体验的一大改进,这也是为什么我相信Signal 是未来的原因。
Signal 的实现并不明显,这就是为什么行业需要很长时间才能达到这一点的原因。Signal 需要与底层框架紧密耦合,以获得最佳的开发体验和性能。
为了获得最佳结果,框架的渲染和 Observable 的更新需要进行协调。因此,我认为不太可能出现独立于框架的通用 Signal 库。
export const Counter = component$(() => {
const count = useSignal(123);
return <button onClick$={() => count.value++}>{count.value}</button>;
});
export const Counter = () => {
const [count, setCount] = createSignal(123);
return <button onClick={() => setCount(count() + 1)}>{count()}</button>;
};
<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 和模板的集成,所以我还没有包含 Angular 的示例。但我喜欢他们的发展方向 - 在我看来是正确的方向。
尽管我有自己的喜好,但所有方法都有优点和缺点,因此存在权衡。让我们先看看优点:
基于值的:
基于 Observable 的:
非常复杂的情况,并且非常适合浏览器事件系统,因为它涉及事件随时间的变化(但不适合于需要使用相同状态重新渲染的 UI)。
基于 Signal 的:
基于值的:
由于 Svelte 的聪明的编译器,性能下降非常小,所以在实践中可能没问题。
基于 Observable 的:
BehaviorSubjects
,允许进行同步读取和写入。subscribe()
不是良好的开发体验,因为它要求为每个绑定位置订阅(分配回调函数)。unsubscribe()
以避免内存泄漏。注意:许多框架可以自动为简单情况创建
subscribe()
/unsubscribe()
调用,但更复杂的情况通常需要开发人员负责订阅。
基于 Signal 的:
Observables(可观察对象)过于复杂,不适合用于用户界面(UI)(因为只有BehaviorSubject
可观察对象在 UI 中真正有效)。因此,我不打算花太多时间讨论它。
我认为基于值(value-based)和基于 Signal(signal-based)的系统之间的权衡是很容易开始 ⇒ 之后出现性能问题 vs. 开始时需要稍微更多的规则(更多知识)⇒ 但之后无需优化。
在基于值的系统中,性能问题是逐渐累积的。没有一个特定的改变会导致应用程序出现问题,只是“有一天它变得太慢了”。由于开发人员往往拥有快速的计算机,而移动用户首先抱怨。一旦想要进行优化,就没有“明显”的问题可解决。
相反,这是多年来积累的债务的一个漫长而缓慢的消减过程。此外,“优化”API 引入了风险,可能会导致你掉入响应式的陷阱(更新停止传播)。
使用 Signal 系统时,需要稍微更深入地了解,可能会掉入响应式的陷阱。然而,掉入陷阱是即时、明显且容易修复的。
如果在使用 Signal 时出现响应式错误,应用程序就会崩溃。这是显而易见的!修复方法也很明显。你没有遵循响应式规则之一,你吸取了教训,也许不会再犯同样的错误。快速学习循环。
一旦开始优化基于值的系统,你就进入了与 Signal 相同的响应式世界,你可能会遇到相同的响应式问题。基于值的“优化”API 本质上是“带有较差开发体验的 Signal”。
因此,你面临的问题是,你想要快速失败还是慢慢失败?我更喜欢快速失败模式。
这是我喜欢 Signal 的第二个原因。 Signal 为你提供了一种可能性,可以可视化系统的响应式图并进行调试。
我认为,尽管 Signal 需要稍微更多的投入,但它们将会随着时间的推移而盛行。这就是为什么我说:“我不知道哪个框架会变得流行(我有自己的喜好),但我确信你的下一个框架将是基于 Signal 的。”