龙空技术网

不懂 S.js 不可能懂信号 Signals!

高级前端进阶 1662

前言:

而今我们对“js获取当前时间周”大概比较讲究,各位老铁们都想要了解一些“js获取当前时间周”的相关文章。那么小编也在网摘上收集了一些对于“js获取当前时间周””的相关内容,希望朋友们能喜欢,朋友们快快来学习一下吧!

家好,很高兴又见面了,我是"高级前端‬进阶‬",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!

高级前端‬进阶

前言

最近拜读了一篇文章《JavaScript 中 Signals 的演变》,里面提到了 S.js。重点提到 S.js是今天信号 Signals 术语的来源,以下是文章的部分内容截取:

S.js 是独立于其他大多数解决方案而开发的,它更直接地以数字电路为模型,所有的状态变化都在时钟周期内进行,S.js 将其状态基元称为 "Signals(信号)"。虽然不是第一次使用这个名字,但它是我们今天使用的术语的来源。

更重要的是,S.js 引入了响应式所有权的概念。一个所有者将收集所有的子响应式作用域,并在所有者自己的 deposal 逻辑或在重新执行时管理它们的 deposal 逻辑。响应式视图将从一个根所有者开始,然后每个节点将作为其后代的所有者。这种所有者模式不仅对 deposal 很有用,而且是在响应式视图中建立 Provider / Consumer 上下文的一种机制。

刚好最近也在研究 Signals,对此非常好奇,以下是已经发表文章的传送门:

《为什么说 useSignal() 是 Web 框架的未来!》《Signals:众多前端框架的选择!是时候放弃状态值了!》

前端同学应该会比较有体感,Signal 最近越来越受欢迎,并且是许多现代 Web 框架的一部分,例如 Solid.js、Preact、Qwik 和 Vue.js。所以今天特地行文来给大家介绍 S.js,希望大家通过对 S.js 的熟悉来更好的、深入理解 Signals。

目前 S.js 在 Github 上有超过 1.1k 的 star、60+的 fork,和前端大火的其他项目相比确实没有什么明显的热度,但是却也是一个值得长期关注的前端项目。话不多说,直接开始!

1.什么是反应式编程

反应式编程是一种编程思想与方式,是为了简化并发编程而出现的。与传统的处理方式相比,反应式编程能够基于数据流中的事件进行反应处理

例如:在 a+b=c 的场景,在传统编程方式下如果 a、b 发生变化,那么需要重新计算 a+b 来得到 c 值。而反应式编程中,不需要重新计算。a、b 的变化事件会触发 c 的值自动更新。这种方式类似于在消息中间件中常见的发布/订阅模式。由流发布事件,而代码逻辑作为订阅方基于事件进行处理,并且是异步执行的。

反应式编程中,最基本的处理单元是事件流(事件流是不可变的,对流进行操作只会返回新的流)中的事件。核心是基于事件流、无阻塞、异步的,使用反应式编程不需要编写底层的并发、并行代码。并且由于其声明式编写代码的方式,使得异步代码易读且易维护。

常用的反应式编程类库包括:Reactor、RxJava 2、Vert.x 以及 Ratpack 等等。

2.S.js及其特征

S.js 是一个小型的反应式编程库,它将自动生成依赖图与同步执行引擎相结合。 目标是使反应式编程简单、简洁、快速。

S.js 应用程序由数据信号(signals)和计算(computations)组成:

数据信号是用 S.data(<value>) 创建的, 它们是一段可能会改变的数据的小容器。计算是用 S(() => <code>) 创建的,随着数据信号的变化,它们会自动保持最新。

两种信号都表示为小函数:调用数据信号以读取其当前值,将新值传递给数据信号以更新它。除此之外,S.js 还有一些实用程序来控制什么是更改以及 S.js 的响应方式。S.js 具有以下显著特点:

自动更新 :当数据信号发生变化时,S 会自动重新运行任何读取旧值的计算。清晰、一致的时间轴 : S 应用程序通过一系列离散的 ticks 修正。 在每个 tick 中,所有信号都保证返回最新值,并且在 tick 完成之前状态是不可变的。 这极大地简化了推理变化如何流经反应式应用程序这一通常最困难的任务。批量更新:多个数据信号可以在一次tick中更改。自动处理:S 计算本身可以创建更多计算,规则是“子”计算在其“父”更新时被处理。 这个简单的规则允许应用程序无泄漏,无需手动处理。3.S.js的简单示例

下面的例子将页面正文设置为文本“Hello, world!”:

let greeting = "Hello",    name = "world";document.body.textContent = `${greeting}, ${name}!`;

现在更改name值:

name = "reactivity";

该页面现在已过时,因为它仍然具有旧名称“Hello,world”,即没有对数据更改做出反应。所以让我们用 S 的包装器来解决这个问题。

let greeting = S.data("Hello"),    name = S.data("world");S(() => document.body.textContent = `${greeting()}, ${name()}!`);

包装器的返回值是被称为信号的小函数,它们是随时间变化的值的容器。我们通过调用它来读取信号的当前值,如果是数据信号,可以通过传入它来设置它的下一个值。

name("reactivity");

S.js 感知到在设置页面文本时读取了 name() 的旧值,因此它重新运行该计算,因为 name() 已更改, 该页面现在显示为“Hello, reactivity!”。

我们已经将开始使用的纯代码转换为小型机器,能够检测并及时了解传入的更改。 数据信号定义了可能看到的变化类型,以及如何响应它们的计算。

4.S.js的API详解4.1 Data SignalsS.data(<value>)

数据信号是单个值的小容器,是信息和变化进入系统的地方。 通过调用它来读取数据信号的当前值,通过传入一个新值来设置下一个值:

const name = S.data("sue");name(); // 返回 "sue"name("emily") // 设置 name() 为 "emily" ,返回 "emily"

数据信号定义应用程序中更改的粒度。 根据需要,可以选择使它们变得细粒度,即仅包含一个原子值,如字符串、数字等或粗粒度,如单个数据信号中的整个大对象。

请注意,当设置数据信号时,表示正在设置下一个值:如果在时间冻结的上下文中设置数据信号,例如在 S.freeze() 或计算体中,那么更改将在 time 之前生效, 这是因为 S 统一的原子瞬间全局时间轴。如果改变立即生效,那么会有一个改变前后,将瞬间一分为二:

const name = S.data("sue");S.freeze(() => {    name("mary");   // *schedules* next value of "mary" and returns "mary"    name();   // 依然返回 "sue"});name(); // 现在返回"mary";

大多数时候,在顶层设置数据信号(在计算或冻结之外),系统会立即生效以说明更改。为数据信号安排两个不同的下一个值是错误的(其中“不同”由 !== 确定):

const name = S.data("sue");S.freeze(() => {    name("emily");    name("emily");     // 可行, "emily" === "emily"    name("jane");   // 报错: 冲突修改: "emily" !== "jane"});

由 S.data() 创建的数据信号总是在设置时触发更改事件,即使新值与旧值相同:

const name = S.data("sue"),    counter = S.on(name, c => c + 1, 0); // counts name() change eventscounter(); // returns 1 to startname("sue"); // 触发三个更改事件,都具有相同的值name("sue");name("sue");counter(); // 这里返回 4
S.value(<value>)

S.value() 与 S.data() 相同,只是它在设置为相同值时不会触发更改事件。它告诉 S“只有这个数据信号的值是重要的,而不是设置的事件。”

const name = S.value("sue"),    counter = S.on(name, c => c + 1, 0);counter(); // returns 1 to startname("sue");// 设置为同样的值counter(); // 依然返回 1, name() 的值没有修改

默认比较器是 ===,但如果其他更合适,可以将自定义比较器作为第二个参数传递:

const user = S.value(sue, (a, b) => a.userId === b.userId);
4.2 ComputationsS(() => <code>)

计算是一段“实时”代码,当数据信号发生变化时,S 将根据需要重新运行该代码。S 立即运行提供的函数,并且在它运行时,S 自动监视它读取的任何信号。 对于 S,函数如下所示:

S(() => {        ... foo() ...    ... bar() ...       ... bleck() ... zog() ...});

如果这些信号中的任何一个发生变化,S 就会安排重新运行计算。注意:函数主体中由于条件分支而未读取的信号不会被记录。 即使先前的执行在不同的分支下确实读取了它们。因此,只有最后一次运行很重要,因为只有那些信号参与创建当前值。

如果其中一些信号是计算,S 保证它们将始终返回当前值。 永远不会得到“陈旧”值,即受上游更改影响但尚未更新的值。

不仅仅是纯函数

传递给 S 的函数不必是纯函数(即没有副作用)。例如,可以如下记录对 name() 的所有更改:

S(() => console.log(name());

每次 name() 更改时,将重新运行并将值重新输出到控制台。从某种意义上说,这扩展了计算的“价值”是什么的概念,包括它产生的副作用。

提示:在编写执行副作用的计算时,S.cleanup() 和 S.on() 可能是实用的方法。 第一可以帮助计算幂等(有效计算的一个很好的属性),第二可以帮助在它们运行时使其清晰。

Computations 创建 Computations

S允许计算用更多的计算来扩展系统:

const isLogging = S.value(true);S(() => {    if (isLogging()) {        S(() => console.log(foo()));        S(() => console.log(bar()));        S(() => console.log(bleck()));    }});

在此示例中,外部“父”或“构造函数”计算定义了应该记录日志的时间,而内部“子”计算分别负责记录单个信号。需要注意的两个重要点:

外部计算只依赖于它自己读取的信号,在本例中只是 isLogging(),而内部计算只依赖于它们的单个信号,分别是 foo()、bar() 或 bleck()。当父级更新时,内部计算会自动处理。 它们可以被认为是计算“值”的一部分,并且与该值一样,仅持续到下一次执行。

因此,如果 isLogging() 更改为 false,外部计算将重新运行,导致内部计算被处理掉,并且由于它们没有重新创建,将停止记录。

同样的模式允许在没有任何 dispose()、unsubscribe() 或 DidUnmount() 处理程序的情况下构建整个 Web 应用程序。 单个 route() 数据信号可以驱动构建当前视图的 router() 计算,包括使视图动态的所有计算。 当 route() 改变时,所有这些计算都保证自动处理。

Computation Roots

由 <code> 创建的计算在调用 dispose 之前一直有效。如果尝试构建不在根计算或父计算下的计算,S 将输出警告。

// 假设这是顶级代码,而不是在Computation中const foo = S(() => bar() + bleck()); // 导致控制台警告S.root(() => {    const foo = S(() => bar() + bleck());   // 无警告})

如上所述,S 应用程序中的大多数计算都是子计算,它们的生命周期由父级控制。 但有两个例外:

真正的顶级计算,不在任何父级之下。在某些极端情况下,可能希望计算的寿命超过其父级的更新周期。

对于第一种情况,S.root() 告诉 S 确实意味着这些计算是顶级的,因此不会记录任何错误。

对于第二种情况,S.root() 让计算脱离其父级。 它们是“孤立的”,并且会一直存在,直到调用 dispose 函数。

5.本文总结

本文主要和大家介绍下 Signals 的起源,即 S.js。除了上文介绍的 S.js 的特性之外,S.js 还有更多高级特性值得深入研究,比如:减少计算(Reducing Computations)、冻结时钟以应用多个更新(S.freeze)、采样信号以避免依赖(S.sample)、清理过时的副作用(S.cleanup)、从计算中设置数据信号等等。

因为篇幅有限,文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!

参考资料

标签: #js获取当前时间周