Angular Reactivity with Signals

前言

事情的起因从上周四 Angular 发的一条推文开始说。
notion image
先来解释下几个生词:
  • Fine-Grained Reactivity 直译过来是细粒度的反应性
  • Reactive Primitive 直译过来是响应元语
 
我们来看看这条推文说了个啥!!
今天,我们很高兴开启我们对 Fine-Grained Reactivity 探索的第一个 PR!
这是允许原型设计和放大即将到来的 RFC 的价值的基础,我们计划将新的 Reactive Primitive 引入 Angular。
 

🚥 Signals 信号

在响应系统里最重要的就是信号,它们是由 getter、setter 和 value 组成。
信号我们看起来不太熟悉,但有的地方也把信号称之为 可观察对象 或者原子,这样一看就熟悉多了。
 
就其他的同类型框架来说,像下面这样使用一个信号:
const [count, setCount] = createSignal(0);

// read a value
console.log(count()); // 0

// set a value
setCount(5);
console.log(count()); //5
在createSignal中可以存各种类型的值,以便于之后检测更新。
而函数执行更新的方法,主要是通过获取对象或者代理来实现的。
在 Vue 中:
// Vue
const count = ref(0)
// read a value
console.log(count.value); // 0

// set a value
count.value = 5;
或者隐藏在编译器后面,像 Svelte:
// Svelte
let count = 0;
// read a value
console.log(count); // 0

// set a value
count = 5;
究其本质,信号像一个事件的发射器,跟 Rxjs 的 next 是一样的,而两者的不同就是订阅的管理方式。

Reactions 反应

这个东西使用过 React 的小伙伴应该很熟悉,它也有很多别名,比如:Effects、Watches 或 Computeds 等。观察我们的信号并在每次它们的值更新时重新运行它们。
 
一般来说都是这么使用的:
首先定义一个信号 createSignal ,给它一个初始值 0,定义变量 count 和 setCount,而 createEffect 则会监听信号的变化,如果信号发生变化,则触发回调。
 
console.log("1. Create Signal");
const [count, setCount] = createSignal(0);

console.log("2. Create Reaction");
createEffect(() => console.log("The count is", count()));

console.log("3. Set count to 5");
setCount(5);

console.log("4. Set count to 10");
setCount(10);


// 执行
1. Create Signal

2. Create Reaction

The count is 0

3. Set count to 5

The count is 5

4. Set count to 10

The count is 10
每次执行信号时,createEffect 都会检测到它并自动订阅。
这些信号可以是任何类型的数据,Reactions 可以用它做任何事情。
其次,更新是同步发生的。在我们可以记录下一条指令之前,反应已经运行。

Fine-Grained Reactivity 细粒度的反应性

所以我们通过之上的信息,发现 Fine-Grained Reactivity 有两部分组成,一个是 Signals ,另一个是 Reactions。
当然,还有关于这部分内容的推导,比如多次执行后性能问题,使用 memo 钩子等处理这种问题。

总结

到这里,大概有一个概念了,Angular做的原型工作主要围绕在 Angular 中添加信号作为反应原语,实际上是添加类似于 React Hooks 做的事情,而上面的例子,基本也可以总结为 useState、useEffect、useMomo等的作用,如果要说更像,那应该是 Preact。
 

这个题案是怎么来的

在 ng-conf 2022 上,Angular 核心人员 Madleina Scheidegger 说:
我们想改变 Angular 的底层变化检测系统。
 
notion image
 

Reactivity with Signals 和 NgZone

Angular 目前的变更类似于一种自上而下的差异变更,而 Reactivity with Signals 它可以接到 Signals 的 setter 和 getter 方法以创建 Reactions 图,从而精确的控制到一个变量的差异从而发生变更。
 
最近我看到了 Vue 创始人 Evan You 的一条推文,其中强调了响应式和响应式原语之间的细微的一个差别:
 
notion image
 
因此,当人们说框架使用“信号”时,他们通常是指它具有 一个“信号”反应原语和当特定信号改变值时,框架能够精度更新 DOM。
 

与 RxJS

Angular 开发会认为应该改进与 RxJS 的Observables。但是为什么 Angular 提案是 Signals 而不是 Observables 呢?我们甚至需要一个反应原语吗?
就平常使用来说,Observable 需要关注订阅的逻辑,就跟 Observable.next() 一样,其实手动的地方很多,而反应原语就变得方便了一些。
 
 

最后看看这个 PR

 
我们已经开始一些原型设计工作,围绕在 Angular 中添加信号作为反应原语,在我们计划很快开放的正式征求意见稿 (RFC) 之前。早期和开放的原型设计符合我们团队的价值观,并使我们能够充分利用 RFC。
 
如果您看过我们在 ng-conf 2022 上的演讲Angular:10 年后的设计回顾,您就会知道我们一直在认真思考框架的一些基本设计决策。去年,我们启动了一项长期研究项目,目标远大:在框架的核心采用细粒度反应。
 
在细粒度的响应式 Web 框架中,组件跟踪它们依赖的应用程序数据模型的哪些部分,并且仅在该模型更改时与 UI 同步。这与 Angular 今天的工作方式有着根本的不同,它使用 zone.js 来触发整个应用程序的全局自上而下的变化检测。
 
我们相信为 Angular 添加内置的反应性可以解锁许多新功能,包括:
  • 数据如何流经应用程序的清晰统一模型。
  • 仅同步需要更新的 UI 部分,甚至低于单个组件的粒度。
  • 显着改进了与 RxJS 等反应式库的互操作性。
  • 更好的护栏可以避免导致变更检测性能不佳的常见陷阱,并避免ExpressionChangedAfterItHasBeenChecked错误等常见痛点。
  • 编写完全无区域应用程序的可行途径,消除了 zone.js 的开销、陷阱和怪癖。
  • 简化了许多框架概念,例如查询和生命周期挂钩。
 
改变像 Angular 这样的既定框架的反应模型是一个重要的项目,面临着许多挑战。我们对该项目的计划分为多个阶段:
  1. 研究和开发,试验不同的反应性方法。
  1. 选择候选设计。
  1. 初始设计的原型制作以证明可行性。
  1. 一个或多个社区征求意见 (RFC) 以探索权衡并为最终设计提供信息。
  1. 核心反应性实现的开发者预览。
  1. 核心实现向完整设计的迭代和扩展。
  1. 与社区合作,将整个 Angular 生态系统带入一个响应式的未来。
 
在过去的几个月里,我们已经完成了这个项目的第一和第二阶段,并且已经集中在一个基于众所周知的反应原语信号的设计上。在我们的实验过程中,我们觉得这个设计展示了与我们总体目标的最强一致性。
 
信号在框架领域并不是一个新概念——Preact、Solid 和 Vue 都采用了这个概念的某个版本并取得了巨大的成功。我们从这些和其他响应式框架中获得了很多灵感,我们特别感谢SolidJS 的Ryan Carniato,感谢他在去年的许多对话中愿意分享他的专业知识和经验。
 
也就是说,跨框架的需求差异很大,我们设计信号版本既能满足 Angular 的特定需求,又能充分利用 Angular 的独特优势。尽管它仍处于原型阶段,但我们的设计中有几个方面让我们感到特别自豪:
  • 惰性评估,既高效又避免了显式批处理操作的需要
  • 不需要不可变数据的计算模型
  • 灵活的效果调度,允许与 Angular 无缝集成
  • 巧妙地使用WeakRef
    • 来避免信号的显式生命周期管理
  • 使用我们的编译器优化各种反应性操作的可能性,例如在执行 DOM 操作时自动最小化回流的调度效果
  • 具有 RxJS 反应性的双向集成故事
 
我们已经开始制作此设计的原型,并将在接下来的几个月内将其集成到 Angular 中。我们致力于我们的开源精神,并计划公开进行这些原型制作工作。这种原型设计对于证明我们在 Angular 中的信号设计是可行的并允许我们发展到社区 RFC 至关重要。RFC 将涵盖使用信号的基本原理和各个部分的详细设计(我们的实现、与 RxJS 的集成以及与此工作相关的其他主题)。
 
我们坚信,向 Angular 添加内置反应性符合框架及其用户的长期最佳利益,并期待在即将发布的 RFC 中分享有关该项目的更多信息。
 

RFC什么时候出?

今年晚些时候,取决于原型制作工作的进展情况。

为什么将信号作为反应原语?

这是一个很好的问题,将在即将发布的 RFC 中进行深入讨论。简而言之,信号具有我们认为在为 Angular 设计的反应性系统中需要的许多属性,包括:
  • 始终可用的值(同步)
  • 读取值不会触发副作用
  • 值之间的一致性(读取不能显示不一致状态)
  • 隐式、低开销订阅
  • 依赖关系的自动和动态跟踪

为什么要在 RFC 之前提交代码?

我们选择在打开 RFC 之前开始制作原型有几个原因:
  1. 我们觉得拥有一个真正的原型会放大我们从 RFC 中获得的价值,因为我们将能够分享更详细的设计并提出更精确的问题。
  1. 在任何主要设计中,都存在挑战和限制,这些挑战和限制只有在完整原型的规模下才能显现出来。我们想揭示这些并在 RFC 本身中说明它们。
  1. 许多技术问题与整个反应性系统设计无关(例如它将如何集成到 Angular 的变更检测算法中)。我们希望尽早解决这些问题。
  1. 谷歌的内部代码库有一套不同于外部生态系统的构建工具和约束,早期原型设计对于在该环境中验证我们的设计也是必要的。