首页 >web前端 >js教程 >反应性是什么鬼!?

反应性是什么鬼!?

DDD
DDD原创
2024-12-22 05:44:10395浏览

反应模型解释

前言

自从我开始开发应用程序和网站以来已经过去了 10 年,但 JavaScript 生态系统从未像今天这样令人兴奋!

2022 年,社区被“信号”的概念所吸引,以至于大多数 JavaScript 框架都将它们集成到自己的引擎中。我正在考虑 Preact,它自 2022 年 9 月以来提供了与组件生命周期分离的反应变量;或者最近的 Angular,它于 2023 年 5 月实验性地实现了 Signals,然后从版本 18 正式开始。其他 JavaScript 库也选择重新考虑他们的方法......

从 2023 年到现在,我一直在各个项目中使用 Signals。它们的实施和使用简单性完全说服了我,以至于我在技术研讨会、培训课程和会议期间与我的专业网络分享了它们的好处。

但最近,我开始问自己这个概念是否真正“革命性”/是否有信号的替代品?因此,我更深入地研究了这种反思,并发现了反应式系统的不同方法。

这篇文章概述了不同的反应模型,以及我对它们如何工作的理解。

注意: 说到这里,你可能已经猜到了,我不会讨论 Java 的“Reactive Streams”;否则,我会把这篇文章的标题定为“背压是什么鬼!?” ?

理论

当我们谈论反应性模型时,我们(首先也是最重要的)谈论“反应性编程”,但特别是“反应性”。

响应式编程是一种开发范例,允许将数据源的更改自动传播给消费者。

因此,我们可以将反应性定义为根据数据的变化实时更新依赖关系的能力。

NB:简而言之,当用户填写和/或提交表单时,我们必须对这些更改做出反应,显示加载组件或任何其他指定正在发生的事情。 .. 另一个例子,当异步接收数据时,我们必须通过显示全部或部分数据、执行新操作等来做出反应

在这种情况下,反应式库提供了自动更新和高效传播的变量,使编写简单且优化的代码变得更加容易。

为了提高效率,当且仅当它们的值发生变化时,这些系统必须重新计算/重新评估这些变量!同样,为了确保广播的数据保持一致和最新,系统必须避免显示任何中间状态(特别是在状态变化的计算期间)。

NB:状态是指程序/应用程序整个生命周期中使用的数据/值。

好吧,但是……这些“反应模型”到底是什么?

PUSH,又名“急切”反应

第一个反应模型称为“PUSH”(或“渴望”反应)。该系统基于以下原则:

  • 数据源的初始化(称为“Observables”)
  • 组件/函数订阅这些数据源(这些是消费者)
  • 当值发生变化时,数据会立即传播给消费者(称为“观察者”)

正如您可能已经猜到的,“PUSH”模型依赖于“Observable/Observer”设计模式。

第一个用例:初始状态和状态更改

让我们考虑以下初始状态,

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;

WTF Is Reactivity !?

使用反应式库(例如 RxJS),这个初始状态看起来更像这样:

let a = observable.of({ firstName: "John", lastName: "Doe" });
const b = a.pipe(map((a) => a.firstName));
const c = a.pipe(map((a) => a.lastName));
const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));

注意:为了这篇文章的目的,所有代码片段都应被视为“伪代码”。

现在,我们假设消费者(例如组件)希望在数据源更新时记录状态 D 的值,

d.subscribe((value) => console.log(value));

我们的组件将订阅数据流;它仍然需要触发改变,

a.next({ firstName: "Jane", lastName: "Doe" });

从那里,“PUSH”系统检测到更改并自动将其广播给消费者。基于上面的初始状态,以下是可能发生的操作的描述:

  • 数据源A发生状态变化!
  • A的值传播到B(数据源B的计算);
  • 然后,将B的值传播到D(数据源D的计算);
  • A的值传播到C(数据源C的计算);
  • 最后将C的值传播到D(数据源D的重新计算);

WTF Is Reactivity !?

该系统的挑战之一在于计算顺序。事实上,根据我们的用例,您会注意到 D 可能会被评估两次:第一次使用 C 的先前状态值;第二次使用 C 的值。第二次,C 的值是最新的!在这种反应性模型中,这个挑战被称为“钻石问题” ️。

第二个用例:下一次迭代

现在,我们假设该州依赖两个主要数据源,

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;

更新 E 时,系统将重新计算整个状态,这使得系统可以通过覆盖之前的状态来保留单一事实来源。

  • 数据源E发生状态变化!
  • A的值传播到B(数据源B的计算);
  • 然后,将B的值传播到D(数据源D的计算);
  • A的值传播到C(数据源C的计算);
  • E的值传播到C(数据源C的重新计算);.
  • 最后将C的值传播到D(数据源D的重新计算);

WTF Is Reactivity !?

“钻石问题”再次发生...这次是在数据源 C 上,可能会评估 2 次,并且始终在 D 上。

钻石问题

“钻石问题”并不是“渴望”反应模型中的新挑战。一些计算算法(尤其是 MobX 使用的算法)可以标记“反应式依赖树的节点”以平衡状态计算。通过这种方法,系统将首先评估“根”数据源(在我们的示例中为 A 和 E),然后是 B 和 C,最后是 D。更改状态计算的顺序有助于解决此类问题。

WTF Is Reactivity !?

PULL,又名“惰性”反应

第二个反应模型称为“PULL”。与“PUSH”模型不同,它基于以下原则:

  • 反应变量的声明
  • 系统推迟状态计算
  • 派生状态是根据其依赖关系计算的
  • 系统避免过度更新

最重要的是要记住最后一条规则:与之前的系统不同,最后一条规则推迟了状态计算,以避免对同一数据源进行多次评估。

第一个用例:初始状态和状态更改

让我们保持之前的初始状态...

WTF Is Reactivity !?

在这种系统中,初始状态语法将采用以下形式:

let a = observable.of({ firstName: "John", lastName: "Doe" });
const b = a.pipe(map((a) => a.firstName));
const c = a.pipe(map((a) => a.lastName));
const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));

注意: React 爱好者可能会认识这个语法 ?

声明一个反应变量给一个元组“诞生”:一方面是不可变的变量;另一个变量的更新函数。其余语句(在我们的例子中为 B、C 和 D)被视为派生状态,因为它们“监听”各自的依赖关系。

d.subscribe((value) => console.log(value));

“惰性”系统的定义特征是它不会立即传播更改,而是仅在明确请求时传播更改。

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;

在“PULL”模型中,使用effect()(来自组件)来记录反应变量(指定为依赖项)的值会触发状态更改的计算:

  • D 将检查其依赖项(B 和 C)是否已更新;
  • B 将检查其依赖项 (A) 是否已更新;
  • A 将其值传播给 B(计算 B 的值);
  • C 将检查其依赖项 (A) 是否已更新;
  • A 会将其值传播给 C(计算 C 的值)
  • B和C将各自的值传播给D(计算D的值);

WTF Is Reactivity !?

在查询依赖项时可以优化该系统。事实上,在上面的场景中,A 被查询了两次以确定它是否已更新。但是,第一个查询可能足以定义状态是否已更改。 C 不需要执行此操作...相反,A 只能广播其值。

第二个用例:下一次迭代

让我们通过添加第二个反应变量“root”来使状态稍微复杂一些,

let a = observable.of({ firstName: "John", lastName: "Doe" });
const b = a.pipe(map((a) => a.firstName));
const c = a.pipe(map((a) => a.lastName));
const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));

系统再次推迟状态计算,直到明确请求为止。使用与之前相同的效果,更新新的反应变量将触发以下步骤:

  • D 将检查其依赖项(B 和 C)是否已更新;
  • B 将检查其依赖项 (A) 是否已更新;
  • C 将检查其依赖项(A 和 E)是否已更新;
  • E 将其值传播给 C,C 将通过记忆获取 A 的值(计算 C 的值);
  • C 将其值传播给 D,D 将通过记忆获取 B 的值(计算 D 的值);

WTF Is Reactivity !?

由于 A 的值没有改变,所以不需要重新计算这个变量(同样的情况也适用于 B 的值)。在这种情况下,使用记忆算法可以提高状态计算期间的性能。

推拉,又名“细粒度”反应

最后一个反应模型是“推拉”系统。术语“PUSH”反映了更改通知的立即传播,而“PULL”指的是按需获取状态值。这种方法与所谓的“细粒度”反应性密切相关,它遵循以下原则:

  • 反应变量的声明(我们谈论的是反应基元)
  • 在原子级别跟踪依赖关系
  • 变更传播具有高度针对性

请注意,这种反应性并非“推拉”模型所独有。细粒度反应性是指对系统依赖性的精确跟踪。因此,还有 PUSHPULL 反应模型也以这种方式工作(我正在考虑 Jotai 或 Recoil。

第一个用例:初始状态和状态更改

仍然基于之前的初始状态...“细粒度”反应系统中初始状态的声明将如下所示:

let a = { firstName: "John", lastName: "Doe" };
const b = a.firstName;
const c = a.lastName;
const d = `${b} ${c}`;

注意:signal关键字的使用不仅仅是轶事

在语法方面,它与“PUSH”模型非常相似,但有一个显着且重要的区别:依赖关系!在“细粒度”反应系统中,没有必要显式声明计算派生状态所需的依赖关系,因为这些状态隐式跟踪它们使用的变量。在我们的例子中,B 和 C 将自动跟踪 A 值的更改,D 将跟踪 B 和 C 的更改。

let a = observable.of({ firstName: "John", lastName: "Doe" });
const b = a.pipe(map((a) => a.firstName));
const c = a.pipe(map((a) => a.lastName));
const d = merge(b, c).pipe(reduce((b, c) => `${b} ${c}`));

在这样的系统中,更新反应变量比基本的“PUSH”模型更有效,因为更改会自动传播到依赖于它的派生变量(仅作为通知,而不是值本身)。

d.subscribe((value) => console.log(value));

然后,根据需要(让我们以 logger 为例),系统内使用 D 将获取关联根状态的值(在我们的例子中为 A),计算值​​导出状态(B和C),最后评估D。这不是一个直观的操作方式吗?

WTF Is Reactivity !?

第二个用例:下一次迭代

让我们考虑以下状态,

a.next({ firstName: "Jane", lastName: "Doe" });

再一次,推拉系统的“细粒度”方面允许自动跟踪每个状态。因此,派生状态 C 现在跟踪根状态 A 和 E。更新变量 E 将触发以下操作:

  • 反应原语 E 的状态变化!
  • 目标更改通知(通过 C 从 E 到 D);
  • E将其值传播给C,C将通过记忆检索A的值(计算C的值);
  • C 会将其值传播给 D,D 将通过 memoization 检索 B 的值(计算 D 的值);

WTF Is Reactivity !?

这是反应性依赖关系相互之间的先前关联,使得该模型如此高效!

确实,在经典的“PULL”系统(例如 React 的 Virtual DOM)中,当从组件更新响应式状态时,框架将收到更改通知(触发“ 差异”阶段)。然后,根据需要(和延迟),框架将通过遍历反应式依赖树来计算更改;每次更新变量时!这种对依赖状态的“发现”需要付出巨大的代价......

通过“细粒度”反应性系统(如信号),反应性变量/基元的更新会自动通知与它们相关的任何派生状态的变化。因此,无需(重新)发现关联的依赖关系;状态传播是有针对性的!

结论(.值)

到 2024 年,大多数 Web 框架都选择重新思考它们的工作方式,特别是在反应性模型方面。这种转变总体上提高了他们的效率和竞争力。其他人选择(仍然)混合(我在这里考虑的是 Vue),这使他们在许多情况下更加灵活。

最后,无论选择什么模型,在我看来,一个(好的)反应式系统是建立在一些主要规则之上的:

  1. 系统防止不一致的派生状态;
  2. 系统中使用状态会导致反应式派生状态;
  3. 系统最大限度地减少过多的工作;
  4. 并且,“对于给定的初始状态,无论状态遵循的路径,系统的最终结果将始终相同! “

最后一点可以解释为声明式编程的基本原则,这就是我如何看待(好的)反应式系统需要确定性!这就是使反应式模型可靠、可预测且易于在大规模技术项目中使用的“决定论”,无论算法有多复杂。

以上是反应性是什么鬼!?的详细内容。更多信息请关注PHP中文网其他相关文章!

声明:
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn