大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发,您的支持是我不断创作的动力。
目前 Angular、Bubble、Ember、FAST、MobX、Preact、Qwik、RxJS、Solid、Starbeam、Svelte、Vue、Wiz等框架作者正在合作开发一个可以支持反应性核心 (Reactivity Core) 的通用模型。接下来一起聊聊什么是 Signals,为什么需要 Signals!
为什么需要 Signals为了开发复杂的用户界面 (UI),JavaScript 应用开发人员通常需要以有效的方式存储 (store)、计算 (compute)、无效 (invalidate)、同步 (sync) 状态并将状态推送到应用程序的视图层。 但是,UI 通常不仅仅涉及管理简单的值,还经常涉及渲染计算状态,该状态依赖于其他值或状态的复杂树,而该树本身也是计算的。
Signals 的目标是提供用于管理此类应用程序状态的基础设施,以便开发人员可以专注于业务逻辑而非重复的细节。独立发现类似信号的构造在非 UI 上下文中也很有用,特别是在构建系统中以避免不必要的重建。
信号用于反应式编程,以消除管理应用程序更新的需要。用于根据状态更改进行更新的声明式编程模型。
pub/sub 模式带来的模板代码爆炸以下示例给定一个变量 counter,无论 counter 是偶数还是奇数,开发者都希望将其渲染到 DOM 中,每当 counter 发生变化时希望使用 parity来更新 DOM。在 Vanilla JS 中,比如下面的示例:
let counter = 0;// setCounter 方法const setCounter = (value) => { counter = value; render();};const isEven = () => (counter & 1) == 0;const parity = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// 模拟 counter 外部更新setInterval(() => setCounter(counter + 1), 1000);以上示例会有以下问题,比如:
counter 设置样板代码繁重counter 状态与渲染系统紧密耦合如果 counter 发生变化但 parity 没有,例如:counter 从 2 变为 4,此时会进行不必要的 parity 计算和渲染如果 UI 只想在 counter 更新时重新渲染怎么办?此时,虽然可以通过引入 pub/sub 来解决,pub/sub 允许 counter 的其他消费者订阅以添加对状态变化的反应。然而,pub/sub 仍然面临以下问题:
依赖于 parity 的 render 函数必须订阅 counter如果不直接与 counter 交互则无法单独基于 isEven 或 parity 更新 UI增加了样板文件,使用时不仅仅是调用函数或读取变量,而是订阅更新的问题,且管理取消订阅也特别复杂总之,以上示例虽可以通过 pub/sub 解决,然后 isEven 必须准确订阅 counter,parity 必须准确订阅 isEven,render 必须准确订阅 parity。从而产生样板代码爆炸,而且还陷入了大量的订阅工作。且如果不以正确的方式清理内容,则可能会导致潜在的内存泄漏。
Signals 从框架中来到 JavaScript 标准中去尽管 JS 或 Web 平台中没有内置任何 Signals 机制,但 Model 和 View 在 UI 渲染中的双向数据绑定长期以来一直是跨多种编程语言的 UI 框架的核心。
在 JS 框架和库中,已经进行了大量的实验来实现此类绑定并且证明了单向数据流 (One-way Data Flow) 与表示状态 (State) 或计算单元 (Computation) 的一流数据类型相结合的强大能力,而这种从其他数据派生的新数据称为 “信号”。
这种一流的反应式( Reactive) 方法在 2010 年的 Knockout 中被首次使用,同时在不同的前端框架中已经衍生了诸多变体和实现,几乎每个现代 JavaScript 库或框架都提供了 Signals 等类似的能力。
以下面的 proposal-signals 提案的 Signal 代码为例:
// Signal 实例表示读取动态变化值的能力,该值的更新会随着时间的推移被跟踪// Signal 还隐式地包括通过来自另一个计算信号的跟踪访问隐式订阅信号的能力const counter = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");// 库或框架定义基于其他信号原语的 effectdeclare function effect(cb: () => void): (() => void);effect(() => element.innerText = parity.get());// 模拟外部对 counter 的更新setInterval(() => counter.set(counter.get() + 1), 1000);Signal 给开发者带来的远不止 API 表面所看到的能力,还包括:
自动依赖性跟踪:计算信号 (Computed Signal) 会自动发现所依赖的任何其他信号,无论这些信号是简单值还是其他计算值惰性求值:计算 (Computation) 在声明时不会立即求值,也不会在依赖项发生变化时立即求值,仅当明确请求其值时才会执行自动缓存:计算信号会缓存其最后的值,以便其依赖项没有变化时无需重新计算DevTools 标准支持:内置信号使 JS 运行时和开发工具能够改进对检查 (Inspecting) 信号的支持,特别是调试或性能分析,无论是内置在浏览器中还是通过扩展程序实现。 从而可以更新元素检查器、性能快照和内存分析器等现有工具,以在信息呈现中专门突出显示信号。其他诸多优势:标准前端库无需自己实现 Signals、HTML/DOM 集成(未来的可能性)等等无限可能性关于 proposal-signals 提案的更多内容可以参考文末资料,本文不再过多展开。
参考资料https://github.com/tc39/proposal-signals
https://tomaszs2.medium.com/signals-to-become-part-of-javascript-64ce72009f69
https://vincenttunru.com/Javascript-reactive-programming/
https://www.youtube.com/watch?app=desktop&v=bUy4xiJ05KY