草庐IT

Signals 在JavaScript中的应用

大转转FE 2023-03-28 原文
最近,"Signals"成为了前端备受关注的话题。很多国外的大佬都发文表示Signals是前端框架的未来。同时,尤大也在Vue官网上添加了"Connection to Signals"部分。此外,包括Solid、Angular、Preact、Qwik和Vue等多个前端框架都已经开始实现Signals。

作为一名FE,如果你和我之前一样还不是很了解Signals,那么这篇文章或许可以帮助你更好地了解一下这个技术。本文将介绍Signals的历史、概念和优势。

一、发展历史

自从声明式JavaScript框架问世以来,Signals机制一直存在。随着时间的推移,它采用了许多不同的名称,经历了多年的流行和消失。

在声明式JavaScript框架中,组件是声明其输出的单元,可以被动态地渲染和组合。这种方式的优点是,它允许开发人员集中精力于组件的输出,而无需担心组件如何被渲染和更新。这种抽象方式也使得组件更容易被复用,并且更容易理解和测试。

然而,这种抽象也带来了一些挑战。其中一个挑战是组件之间如何通信和共享状态。这些问题可能导致代码变得笨重,难以维护,并且在复杂的应用程序中容易出现混乱。因此,开发人员需要一种更灵活、更强大的通信机制来解决这些问题。

在这种情况下,Signals机制成为了一个有用的解决方案。Signals机制允许组件在不直接引用其他组件的情况下通信,并且能够更灵活地传递消息和状态。这些机制可以是事件、回调、Promise或其他异步机制。它们可以被用来处理各种不同的场景,例如用户交互、网络请求和状态更改等。

Signals机制还具有许多其他优点。它们可以提高应用程序的可维护性和可扩展性,并且可以帮助开发人员更好地理解和调试代码。此外,由于Signals机制允许组件之间松散耦合,因此它们也有助于提高代码的可重用性。

1.1 早期实现

有时令人惊讶的是,多个团队几乎在同一时间达成了相似的解决方案。声明式JavaScript框架的开端有三个版本:Knockout.js(2010年7月),Backbone.js(2010年10月)和Angular.js(2010年10月)。

Angular的脏检查,Backbone的模型驱动的重渲染,以及Knockout的细粒度更新。每一个都略有不同,但最终都将成为我们今天管理状态和更新DOM的基础。

1.2 数据绑定

Angular.js里面常用的模式叫作数据绑定。数据绑定是将部分状态(state)附加到视图树(view tree)某个特定部分的一个方法。可以做到的一个强大的事情是使其成为双向的。因此,我们可以让状态更新 DOM,反过来,DOM 事件自动更新状态,所有这些都是以一种简单的声明方式进行的。但是如果滥用也会出现问题,在 Angular 中,如果不知道有什么变化,就会对整个树进行肮脏的检查,向上传播可能会导致它发生多次。

1.3 Mobx

之后就是react的时代,react对状态管理没有太多的限制。MobX就是这种解决方案。它强调一致性和无障碍传播。也就是说,对于任何给定的变化,系统的每一部分都只运行一次,而且是以适当的顺序同步运行。

它通过将先前方案中典型的基于 push 的响应式换成 push-pull 混合系统来做到这一点。变化的通知被推送出去,但派生状态的执行被推迟到读取它的地方。

1.4 Vue

Vue(2014) 也为今天的发展提供了巨大的贡献。除了在优化一致性方面与 MobX 保持一致外,Vue从一开始就将「细粒度」的响应性作为其核心。

虽然 Vue 与 React 共享虚拟 DOM 的使用,但响应性是一流的,这意味着它首先作为一种内部机制与框架一起开发,以支持其 Options API,并在过去几年中,成为 Composition API 的核心 (2020)。

Vue 通过调度任务,将 pull / push 机制向前推进了一步。默认情况下,Vue 的修改不会立马被执行,而是要等到下一个微任务才会执行。

然而,这种调度也可以用来做一些其他的事情,比如 keep-alive,以及 Suspense。甚至像并发渲染这样的事情也可以用这种方法来实现,真正展示了如何获得基于 pull 和 push 的两种方法的最佳效果。

二、为什么是Signals

Signals 的独特之处在于状态更改会以最有效的方式来自动更新组件和 UI。Signals 基于自动状态绑定和依赖跟踪提供了出色的工效,并具有针对虚拟 DOM 优化的独特实现。

2.1 状态管理的困境

随着应用越来越复杂,项目中的组件也会越来越多,需要管理的状态也越来越多。

为了实现组件状态共享,一般需要将状态提升到组件的共同的祖先组件里面,通过 props 往下传递,带来的问题就是更新时会导致所有子组件跟着更新,需要配合 memo 和 useMemo 来优化性能。

虽然这听起来还挺合理,但随着项目代码的增加,我们很难确定这些优化应该放到哪里。

即使添加了 memoization,也常常因为依赖值不稳定变得无效,由于 Hooks 没有可以用于分析的显式依赖关系树,所以也没法使用工具来找到原因。

另一种解决方案就是放到 Context 上面,子组件作为消费者自行通过 useContext 来获取需要的状态。

但是有一个问题,只有传给 Provider 的值才能被更新,而且只能作为一个整体来更新,无法做到细粒度的更新。

为了处理这个问题,只能将 Context 进行拆分,业务逻辑又不可避免地会依赖多个 Context,这样就会出现 Context 套娃现象。

2.2 通向未来的 Signals

Signal 的核心是一个通过.value属性 来保存值的对象。它有一个重要特征,那就是 Signal 对象的值可以改变,但 Signal 本身始终保持不变。

import { signal } from "@preact/signals";

const count = signal(0);

// Read a signal’s value by accessing .value:
console.log(count.value); // 0

// Update a signal’s value:
count.value += 1;

// The signal's value has changed:
console.log(count.value); // 1

在 Preact 中,当 Signal 作为 props 或 context 向下传递时,传递的是对 Signal 的引用。这样就可以在不重新渲染组件的情况下更新 Signal,因为传给组件的是 Signal 对象而不是它的值。

这让我们可以跳过所有昂贵的渲染工作,立即跳到任意访问 signal.value 属性的组件。

Signals 具有第二个重要特征,即它们会跟踪其值何时被访问以及何时被更新。在 Preact 中,当 Signal 的值发生变化时,从组件内访问 Signal 的属性会自动重新渲染组件。

通过Preact的使用,我们可以总结Signals 几点特点:1、感觉上像是使用原始数据结构 2、能根据值的变化自动更新 3、直接更新 DOM (换句话来说无 VDOM) 4、没有依赖数组

三、在SolidJS中的使用

const Greeting = (props) => (
<>Hi <span>{props.name}</span></>
);

const App(() => {
const [visible, setVisible] = createSignal(false),
[name, setName] = createSignal("Josephine");

return (
<div notallow={() => setName("Geraldine")}>{
visible() && <Greeting name={ name } />
}</div>
);
});

render(App, document.body);

可以看到 SolidJS 响应式也是Signal 作为基础,createSignal 既可以用于组件内,也可以用于组件外,这个跟 Preact 中类似。一方面可以将 Signal 作为组件的 local state,也可以定义为 global State。与前面类似,SolidJS 中也有以下相似点:

  • 响应式细粒度更新
  • 无需定义 dependencies
  • 惰性取值
SolidJS 与 Mobx 和 Vue 的响应式非常相似,但是不会处理 VDOM,而是直接更新 DOM。

四、手动实现一个

响应式状态管理三要素,Signals、Reactions、Derivations

// context 包含Reactions中的执行方法和Signal依赖
const context = [];

const createSignal = (value) => {
const subscriptions = new Set();
const readFn = () => {
const running = context.pop();
if (running) {
subscriptions.add({
execute: running.execute
});
running.deps.add(subscriptions);
}
return value;
};
const writeFn = (newValue) => {
value = newValue;
for (const sub of [...subscriptions]) {
sub.execute();
}
};
return [readFn, writeFn];
};

const createEffect = (fn) => {
const execute = () => {
running.deps.clear();
context.push(running);
try {
fn();
} finally {
context.pop(running);
}
};

const running = {
execute,
deps: new Set()
};
execute();
};

const createMemo = (fn) => {
const [memo, setMemo] = createSignal();
createEffect(() => setMemo(fn()));
return memo;
};

const [name, setName] = createSignal("a");
const fullName = createMemo(() => {
return "c-" + name();
});
createEffect(() => console.log(name(), fullName()));
setName("b");
Signals是一个基础的数据更新与读取,Reactions 是可以追踪订阅到 Signals 的变化,所以在 Reactions 函数里设置 Derivations 的值。

五、最后

本文是学习Signals的一些记录。希望能通过介绍响应式状态管理的一些历史和理念,让你对状态管理有全面的认识,如果感觉本文介绍的不够详细,可以阅读下面的引用原文。

六、引用

  1. ​https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob​
  2. ​https://dev.to/this-is-learning/react-vs-signals-10-years-later-3k71​
  3. ​https://mp.weixin.qq.com/s/Tn0rbkCdFw4f-3ihKUEYQA​
  4. ​https://indepth.dev/posts/1289/solidjs-reactivity-to-rendering​
  5. ​https://preactjs.com/guide/v10/signals/​
  6. ​https://preactjs.com/blog/introducing-signals​

有关Signals 在JavaScript中的应用的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby - 其他文件中的 Rake 任务 - 2

    我试图在一个项目中使用rake,如果我把所有东西都放到Rakefile中,它会很大并且很难读取/找到东西,所以我试着将每个命名空间放在lib/rake中它自己的文件中,我添加了这个到我的rake文件的顶部:Dir['#{File.dirname(__FILE__)}/lib/rake/*.rake'].map{|f|requiref}它加载文件没问题,但没有任务。我现在只有一个.rake文件作为测试,名为“servers.rake”,它看起来像这样:namespace:serverdotask:testdoputs"test"endend所以当我运行rakeserver:testid时

  3. ruby-on-rails - Ruby net/ldap 模块中的内存泄漏 - 2

    作为我的Rails应用程序的一部分,我编写了一个小导入程序,它从我们的LDAP系统中吸取数据并将其塞入一个用户表中。不幸的是,与LDAP相关的代码在遍历我们的32K用户时泄漏了大量内存,我一直无法弄清楚如何解决这个问题。这个问题似乎在某种程度上与LDAP库有关,因为当我删除对LDAP内容的调用时,内存使用情况会很好地稳定下来。此外,不断增加的对象是Net::BER::BerIdentifiedString和Net::BER::BerIdentifiedArray,它们都是LDAP库的一部分。当我运行导入时,内存使用量最终达到超过1GB的峰值。如果问题存在,我需要找到一些方法来更正我的代

  4. ruby-on-rails - Rails 3 中的多个路由文件 - 2

    Rails2.3可以选择随时使用RouteSet#add_configuration_file添加更多路由。是否可以在Rails3项目中做同样的事情? 最佳答案 在config/application.rb中:config.paths.config.routes在Rails3.2(也可能是Rails3.1)中,使用:config.paths["config/routes"] 关于ruby-on-rails-Rails3中的多个路由文件,我们在StackOverflow上找到一个类似的问题

  5. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  6. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  7. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  8. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  9. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  10. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

随机推荐