反应模型解释
前言
自从我开始开发应用程序和网站以来已经过去了 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}`;
使用反应式库(例如 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的重新计算);
该系统的挑战之一在于计算顺序。事实上,根据我们的用例,您会注意到 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的重新计算);
“钻石问题”再次发生...这次是在数据源 C 上,可能会评估 2 次,并且始终在 D 上。
钻石问题
“钻石问题”并不是“渴望”反应模型中的新挑战。一些计算算法(尤其是 MobX 使用的算法)可以标记“反应式依赖树的节点”以平衡状态计算。通过这种方法,系统将首先评估“根”数据源(在我们的示例中为 A 和 E),然后是 B 和 C,最后是 D。更改状态计算的顺序有助于解决此类问题。
PULL,又名“惰性”反应
第二个反应模型称为“PULL”。与“PUSH”模型不同,它基于以下原则:
- 反应变量的声明
- 系统推迟状态计算
- 派生状态是根据其依赖关系计算的
- 系统避免过度更新
最重要的是要记住最后一条规则:与之前的系统不同,最后一条规则推迟了状态计算,以避免对同一数据源进行多次评估。
第一个用例:初始状态和状态更改
让我们保持之前的初始状态...
在这种系统中,初始状态语法将采用以下形式:
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的值);
在查询依赖项时可以优化该系统。事实上,在上面的场景中,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 的值);
由于 A 的值没有改变,所以不需要重新计算这个变量(同样的情况也适用于 B 的值)。在这种情况下,使用记忆算法可以提高状态计算期间的性能。
推拉,又名“细粒度”反应
最后一个反应模型是“推拉”系统。术语“PUSH”反映了更改通知的立即传播,而“PULL”指的是按需获取状态值。这种方法与所谓的“细粒度”反应性密切相关,它遵循以下原则:
- 反应变量的声明(我们谈论的是反应基元)
- 在原子级别跟踪依赖关系
- 变更传播具有高度针对性
请注意,这种反应性并非“推拉”模型所独有。细粒度反应性是指对系统依赖性的精确跟踪。因此,还有 PUSH 和 PULL 反应模型也以这种方式工作(我正在考虑 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。这不是一个直观的操作方式吗?
第二个用例:下一次迭代
让我们考虑以下状态,
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 的值);
这是反应性依赖关系相互之间的先前关联,使得该模型如此高效!
确实,在经典的“PULL”系统(例如 React 的 Virtual DOM)中,当从组件更新响应式状态时,框架将收到更改通知(触发“ 差异”阶段)。然后,根据需要(和延迟),框架将通过遍历反应式依赖树来计算更改;每次更新变量时!这种对依赖状态的“发现”需要付出巨大的代价......
通过“细粒度”反应性系统(如信号),反应性变量/基元的更新会自动通知与它们相关的任何派生状态的变化。因此,无需(重新)发现关联的依赖关系;状态传播是有针对性的!
结论(.值)
到 2024 年,大多数 Web 框架都选择重新思考它们的工作方式,特别是在反应性模型方面。这种转变总体上提高了他们的效率和竞争力。其他人选择(仍然)混合(我在这里考虑的是 Vue),这使他们在许多情况下更加灵活。
最后,无论选择什么模型,在我看来,一个(好的)反应式系统是建立在一些主要规则之上的:
- 系统防止不一致的派生状态;
- 在系统中使用状态会导致反应式派生状态;
- 系统最大限度地减少过多的工作;
- 并且,“对于给定的初始状态,无论状态遵循的路径,系统的最终结果将始终相同! “
最后一点可以解释为声明式编程的基本原则,这就是我如何看待(好的)反应式系统需要确定性!这就是使反应式模型可靠、可预测且易于在大规模技术项目中使用的“决定论”,无论算法有多复杂。
以上是反应性是什么鬼!?的详细内容。更多信息请关注PHP中文网其他相关文章!

JavaScript字符串替换方法详解及常见问题解答 本文将探讨两种在JavaScript中替换字符串字符的方法:在JavaScript代码内部替换和在网页HTML内部替换。 在JavaScript代码内部替换字符串 最直接的方法是使用replace()方法: str = str.replace("find","replace"); 该方法仅替换第一个匹配项。要替换所有匹配项,需使用正则表达式并添加全局标志g: str = str.replace(/fi

因此,在这里,您准备好了解所有称为Ajax的东西。但是,到底是什么? AJAX一词是指用于创建动态,交互式Web内容的一系列宽松的技术。 Ajax一词,最初由Jesse J创造

10款趣味横生的jQuery游戏插件,让您的网站更具吸引力,提升用户粘性!虽然Flash仍然是开发休闲网页游戏的最佳软件,但jQuery也能创造出令人惊喜的效果,虽然无法与纯动作Flash游戏媲美,但在某些情况下,您也能在浏览器中获得意想不到的乐趣。 jQuery井字棋游戏 游戏编程的“Hello world”,现在有了jQuery版本。 源码 jQuery疯狂填词游戏 这是一个填空游戏,由于不知道单词的上下文,可能会产生一些古怪的结果。 源码 jQuery扫雷游戏

本教程演示了如何使用jQuery创建迷人的视差背景效果。 我们将构建一个带有分层图像的标题横幅,从而创造出令人惊叹的视觉深度。 更新的插件可与JQuery 1.6.4及更高版本一起使用。 下载

Matter.js是一个用JavaScript编写的2D刚体物理引擎。此库可以帮助您轻松地在浏览器中模拟2D物理。它提供了许多功能,例如创建刚体并为其分配质量、面积或密度等物理属性的能力。您还可以模拟不同类型的碰撞和力,例如重力摩擦力。 Matter.js支持所有主流浏览器。此外,它也适用于移动设备,因为它可以检测触摸并具有响应能力。所有这些功能都使其值得您投入时间学习如何使用该引擎,因为这样您就可以轻松创建基于物理的2D游戏或模拟。在本教程中,我将介绍此库的基础知识,包括其安装和用法,并提供一

本文演示了如何使用jQuery和ajax自动每5秒自动刷新DIV的内容。 该示例从RSS提要中获取并显示了最新的博客文章以及最后的刷新时间戳。 加载图像是选择

本文讨论了在浏览器中优化JavaScript性能的策略,重点是减少执行时间并最大程度地减少对页面负载速度的影响。


热AI工具

Undresser.AI Undress
人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover
用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool
免费脱衣服图片

Clothoff.io
AI脱衣机

AI Hentai Generator
免费生成ai无尽的。

热门文章

热工具

Dreamweaver Mac版
视觉化网页开发工具

螳螂BT
Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

PhpStorm Mac 版本
最新(2018.2.1 )专业的PHP集成开发工具

SublimeText3汉化版
中文版,非常好用

mPDF
mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),