2032 年了,面试官居然还在问三大框架响应式的区别……

首发于公众号 前端从进阶到入院,欢迎关注。

2023 年了,我即将跑路的同事出去面试的时候,告诉我发现面试官还在问“不同框架的响应式有什么区别”这样老生常谈的问题!

正好最近看到 Qwik 的作者 Miško Hevery 分享了自己的一些见解,非常简洁清晰,学完了可以直接对付面试官了。

以下是我整理的原文:

我想分享一下我对当前响应式方法和格局的理解。以下是我个人的观点和意见,其中一些可能有些激进,所以做好准备。(我并不是说我的观点是正确的,但这就是我对这个世界的看法。)

我认为通过分享自己的观点,我们可以在行业中达成共识,我希望这些我多年来辛苦获得的见解对他人有所帮助,可以补充他们对问题的理解中的缺失部分。此外,我非常重视反馈,毕竟即使经过这么多年,我的理解也更像是一个精心编织的网络,而不是坚固的钢笼。

响应式的三位一体

我认为迄今为止,在行业中有三种基本的响应式方法:

  1. 基于值(Value-based);即脏检查(Angular、React、Svelte)
  2. 基于 Observable:(Angular 使用 RxJS、Svelte)
  3. 基于 Signal:(Signals 加持的 Angular、Qwik、MobX 加持的 React、Solid、Vue)

基于值(Value-based)

基于值的系统依赖于将状态存储在本地(非可观察)引用中,作为简单的值。

当我说“可观察”时,我并不是指像 RxJS 这样的 Observables。我指的是可观察这个词的常见用法,即知道何时发生变化。而“非可观察”意味着没有办法知道值在具体的时间点上发生了变化。

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 中。

032

由于值是以一种不允许框架观察到的方式存储的,每个框架都需要一种方式来检测这些值的变化并将组件标记为”dirty”。

一旦标记为”dirty”,组件会重新运行,以便框架可以重新读取/重新创建这些值,从而检测哪些部分发生了变化,并将变化反映到 DOM 中。

🌶️ 小抄:脏检查是值为基础的系统唯一可用的策略。将最新已知值与当前值进行比较。这就是方法。

你如何知道何时运行脏检查算法?

  • Angular( Signal 之前)=> 隐式依赖于zone.js来检测状态可能已发生变化的时机(由于依赖于zone.js的隐式检测,它比严格所需的更频繁地运行变更检测)。
  • React => 显式依赖于开发人员调用setState()
  • Svelte => 在状态赋值周围使用编译器保护/失效(本质上是自动生成setState()调用)。

基于 Observable 的

Observables 是随时间变化的值。Observables 允许框架知道值发生变化的具体时间点,因为将新值推送到 Observable 需要一个作为守卫的特定 API。

Observables 是解决细粒度响应式问题的明显方法,但是它们的开发体验不是最好的,因为 Observables 需要显式调用.subscribe()和相应的.unsubscribe()。Observables 也不能保证同步的无故障传递,这给偏向同步(事务性)更新的 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() {
    // 更新计数值
    count.update(n => n + 1);
  }
</script>

<div>
  <h1>Counter: {$count}</h1>
  <button on:click={increment}>Increment</button>
</div>

Svelte:有趣的是,它有两种具有不同思维模型和语法的响应式系统。这是因为基于值的模型只适用于.svelte文件,所以将代码移出.svelte文件需要其他的响应式原语(Stores)。

我认为每个框架应该有一个单一的响应式模型,可以处理所有的用例,而不是基于用例的不同响应式系统的组合。

基于 Signal 的

Signal 类似于 Observable 的同步版本,但没有 subscribe/unsubscribe。我认为这是开发体验的一大改进,这也是为什么我相信Signal 是未来的原因。

Signal 的实现并不明显,这就是为什么行业需要很长时间才能达到这一点的原因。Signal 需要与底层框架紧密耦合,以获得最佳的开发体验和性能。

为了获得最佳结果,框架的渲染和 Observable 的更新需要进行协调。因此,我认为不太可能出现独立于框架的通用 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 和模板的集成,所以我还没有包含 Angular 的示例。但我喜欢他们的发展方向 – 在我看来是正确的方向。

权衡

尽管我有自己的喜好,但所有方法都有优点和缺点,因此存在权衡。让我们先看看优点:

基于值的

  • 它可以正常工作:值为基础的系统”就能工作”。你不必将对象包装在特殊的容器中,它们易于传递,并且易于进行类型推断(TypeScript)。
  • 难以犯错:作为”就能工作”的推论,它很难掉入响应式的陷阱。你可以以多种不同的方式编写代码并获得预期的结果。
  • 易于解释的思维模型:上述结果的后果易于解释。

基于 Observable 的

  • 值随时间变化的概念非常有吸引力,可以表达

非常复杂的情况,并且非常适合浏览器事件系统,因为它涉及事件随时间的变化(但不适合于需要使用相同状态重新渲染的 UI)。

基于 Signal 的

  • 总是高性能/无需优化:开箱即用的性能。
  • 非常适合 UI 事务/同步更新模型。

基于值的

  • 性能陷阱:性能随时间下降,需要进行”优化重构”,从而产生”性能专家”。因此,这些框架提供了”优化”/”逃生口”的 API 来提高性能。
  • 一旦开始进行优化,就有可能掉入”响应式陷阱”(UI 停止更新),在这方面与 Signal 相同。

由于 Svelte 的聪明的编译器,性能下降非常小,所以在实践中可能没问题。

基于 Observable 的

  • Observables 不适合 UI。UI 表示的是当前要显示的值,而不是随时间变化的值。因此,我们有了BehaviorSubjects,允许进行同步读取和写入。
  • Observables 很复杂。很难解释。有一些专门讲授 Observables 的课程。
  • 显式的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 的。”

032

参考:www.builder.io/blog/unifie…

首发于公众号 前端从进阶到入院,欢迎关注。

原文链接:https://juejin.cn/post/7246777535043256376 作者:ssh_晨曦时梦见兮

(0)
上一篇 2023年6月21日 上午10:36
下一篇 2023年6月21日 上午10:47

相关推荐

发表回复

登录后才能评论