前言
事情的起因从上周四 Angular 发的一条推文开始说。

先来解释下几个生词:
- 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 的底层变化检测系统。

Reactivity with Signals 和 NgZone
Angular 目前的变更类似于一种自上而下的差异变更,而 Reactivity with Signals 它可以接到 Signals 的 setter 和 getter 方法以创建 Reactions 图,从而精确的控制到一个变量的差异从而发生变更。
最近我看到了 Vue 创始人 Evan You 的一条推文,其中强调了响应式和响应式原语之间的细微的一个差别:

因此,当人们说框架使用“信号”时,他们通常是指它具有 一个“信号”反应原语和当特定信号改变值时,框架能够精度更新 DOM。
与 RxJS
Angular 开发会认为应该改进与 RxJS 的
Observables
。但是为什么 Angular 提案是 Signals 而不是 Observables 呢?我们甚至需要一个反应原语吗?就平常使用来说,Observable 需要关注订阅的逻辑,就跟 Observable.next() 一样,其实手动的地方很多,而反应原语就变得方便了一些。
更深入的了解:我改变了主意。Angular 需要一个反应原语
最后看看这个 PR
我们已经开始一些原型设计工作,围绕在 Angular 中添加信号作为反应原语,在我们计划很快开放的正式征求意见稿 (RFC) 之前。早期和开放的原型设计符合我们团队的价值观,并使我们能够充分利用 RFC。
如果您看过我们在 ng-conf 2022 上的演讲Angular:10 年后的设计回顾,您就会知道我们一直在认真思考框架的一些基本设计决策。去年,我们启动了一项长期研究项目,目标远大:在框架的核心采用细粒度反应。
在细粒度的响应式 Web 框架中,组件跟踪它们依赖的应用程序数据模型的哪些部分,并且仅在该模型更改时与 UI 同步。这与 Angular 今天的工作方式有着根本的不同,它使用 zone.js 来触发整个应用程序的全局自上而下的变化检测。
我们相信为 Angular 添加内置的反应性可以解锁许多新功能,包括:
- 数据如何流经应用程序的清晰统一模型。
- 内置框架支持声明性派生状态(一个常见的功能请求)。
- 仅同步需要更新的 UI 部分,甚至低于单个组件的粒度。
- 显着改进了与 RxJS 等反应式库的互操作性。
- 更好的护栏可以避免导致变更检测性能不佳的常见陷阱,并避免
ExpressionChangedAfterItHasBeenChecked
错误等常见痛点。
- 编写完全无区域应用程序的可行途径,消除了 zone.js 的开销、陷阱和怪癖。
- 简化了许多框架概念,例如查询和生命周期挂钩。
改变像 Angular 这样的既定框架的反应模型是一个重要的项目,面临着许多挑战。我们对该项目的计划分为多个阶段:
- 研究和开发,试验不同的反应性方法。
- 选择候选设计。
- 初始设计的原型制作以证明可行性。
- 一个或多个社区征求意见 (RFC) 以探索权衡并为最终设计提供信息。
- 核心反应性实现的开发者预览。
- 核心实现向完整设计的迭代和扩展。
- 与社区合作,将整个 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 之前开始制作原型有几个原因:
- 我们觉得拥有一个真正的原型会放大我们从 RFC 中获得的价值,因为我们将能够分享更详细的设计并提出更精确的问题。
- 在任何主要设计中,都存在挑战和限制,这些挑战和限制只有在完整原型的规模下才能显现出来。我们想揭示这些并在 RFC 本身中说明它们。
- 许多技术问题与整个反应性系统设计无关(例如它将如何集成到 Angular 的变更检测算法中)。我们希望尽早解决这些问题。
- 谷歌的内部代码库有一套不同于外部生态系统的构建工具和约束,早期原型设计对于在该环境中验证我们的设计也是必要的。