Angular Signals:响应式编程的新思维模式,不仅是新API
Angular Signals不仅仅是另一个功能。它们代表了一种不同的数据流思考方式。如果你来自RxJS或标准的@Input()/@Output()绑定,可能会将Signals视为可观察值的更简单语法。然而,这就像说小提琴只是更小的大提琴。乐器的形状影响你创作的音乐类型。
在本文中,我不会重复文档或引导你完成另一个计数器示例。相反,我将介绍Signals启用的一种新思维方式,以及我在生产中使用它们时遇到的一些实际挑战。
Signals作为响应式变量,而非流
将Signals视为响应式变量,而不是数据流。这是视角的关键变化。在RxJS中,我们通常将值推送到流中,组合它们,并通过subscribe()响应副作用。Signals扭转了这个概念。你像变量一样读取它们。Angular自动跟踪依赖关系并触发反应。
以下是我在代码中解释Signals的最佳方式:
|
|
在这个例子中,当firstName或lastName更改时,fullName会自动重新计算。你不需要考虑map、combineLatest或拆卸逻辑。你只需声明关系。
如果这感觉像Vue或SolidJS,那并非巧合。
陷阱1:隐式依赖可能适得其反
当你在computed()或effect()中读取Signal时,Angular会将该读取作为依赖关系进行跟踪。但当你没有意识到这些读取时,这可能会很快出错。
|
|
你可能期望这仅在计数器更改时运行,但如果你在同一函数中意外读取另一个Signal(例如日志标志),它也会成为依赖关系。突然之间,切换调试模式标志开始重新计算你的数学逻辑。
提示:保持computed和effect逻辑狭窄且确定。否则,你将无法调试幽灵更新。
Signals与RxJS:Signals的闪光点与不足
让我们明确一点:Signals不会取代RxJS。它们设计为与RxJS协同工作。但理解何时使用每个至关重要。
使用案例 | 首选Signals | 首选RxJS |
---|---|---|
本地组件状态 | ✓ | X |
派生UI数据 | ✓ | X |
事件流(例如用户输入) | X | ✓ |
跨模块共享状态(通过服务Signals) | ✓ | ✓ |
具有重试的复杂异步流 | X | ✓ |
Signals擅长建模随时间变化的值。RxJS擅长建模随时间变化的事件。
陷阱2:计算信号的缓存不如你所想
我发现的一个令人惊讶的事情:computed()不会像React的useMemo()那样记忆化,甚至不像你从getter中期望的那样。
每次你从computed()信号读取时,如果其输入已更改,逻辑会重新运行。但如果你在模板中多次调用它(例如在*ngIf中并在{{ }}中再次调用),你可能会多次支付成本。
提示:如果计算成本高昂,请考虑将其存储在组件类的本地const中,并在模板中仅引用该const。或者将其包装在另一个信号中。
重新思考状态形状:Signals喜欢扁平,而非深层
在使用服务和RxJS的经典Angular中,通常这样建模状态:
|
|
在Signals中,深度嵌套的响应式对象很尴尬。你不能说user().settings().theme()——那是对读取的读取的读取。相反,你想要扁平化:
|
|
提示:用其自己的信号建模每个状态片段。你将获得灵活性和更轻松的响应式控制。
实际场景:表单标签定制
假设你有一个SearchSidebarComponent,并且想要从父级自定义其标签。以下是天真的方式:
|
|
如果你尝试派生计算值会发生什么?
|
|
现在,假设在父级中你写:
|
|
这有效,但你在信号内调用信号。如果你将模板更改为<search-sidebar [labels]="labelsFinal" />
,它会因类型不匹配而失败。
提示:Angular的输入系统尚未完全信号原生。在此之前,在传递输入之前扁平化值。
陷阱3:effect()立即运行——可能再次运行
与仅在发出内容时触发的RxJS subscribe()不同,effect()在创建时触发一次,即使信号尚未更改。
|
|
即使userId尚未更改,这也会立即运行一次。在effect()中放置HTTP调用或分析跟踪等副作用时要小心。
提示:如果需要,使用null检查或提前返回来保护effect()逻辑。
最终思考
Angular中的Signals不仅仅是新语法,它们是思维模式的转变。一旦你停止思考可观察对象,开始思考响应的变量,你会发现你的组件更小、更快、更易于推理。
但像任何新工具一样,Signals带有锐利的边缘。了解权衡,学习模式,最重要的是,不要将Signals视为花哨的getter。它们比那强大得多,但前提是你理解模型。