话不多说,直接开始!
反应式编程是一种编程思想与方式,是为了简化并发编程而出现的。与传统的处理方式相比,反应式编程能够基于数据流中的事件进行反应处理。
例如:在 a+b=c 的场景,在传统编程方式下如果 a、b 发生变化,那么需要重新计算 a+b 来得到 c 值。而反应式编程中,不需要重新计算。a、b 的变化事件会触发 c 的值自动更新。这种方式类似于在消息中间件中常见的发布/订阅模式。由流发布事件,而代码逻辑作为订阅方基于事件进行处理,并且是异步执行的。
反应式编程中,最基本的处理单元是事件流(事件流是不可变的,对流进行操作只会返回新的流)中的事件。核心是基于事件流、无阻塞、异步的,使用反应式编程不需要编写底层的并发、并行代码。并且由于其声明式编写代码的方式,使得异步代码易读且易维护。
常用的反应式编程类库包括:Reactor、RxJAVA 2、Vert.x 以及 Ratpack 等等。
当 React 被引入时,比其他任何库和框架都更加吸引开发者,它引入了一个非常有趣的概念称为单向数据绑定,或者更简单地说,作为虚拟 DOM 的一部分引入的单向数据流。
它提供了一种全新的体验,即当数据状态发生变化时,开发人员不必考虑更新如何在 UI 中流动。然而,随着越来越多的 hooks 被引入,有一些语法规则可以确保它们以最佳方式执行。 从本质上讲,与 React 的原始目的有偏差,即单向流或显式突变(explicit mutations)。比如:
然而,如果无法有效、正确地避免以上情况,可能导致一些严重的性能问题,即一股脑的重新渲染。 这与仅编写组件来构建 UI 的初衷略有不同。
props drilling 是数据以 props 的形式从 React 组件树中的一部分传递到另一部分, 只是传递的组件层级过深、而中间层组件并不需要这些 props,只是做一个向下转发, 这种情况就叫做 props drilling。
信号(Signals)采用反应式编程原语来帮助消除复杂性,并通过将注意力转移到正确的事情上来帮助改善开发人员体验,而不必明确遵循一组语法规则来获得性能提升。
信号是反应式编程的关键原语(Primitive)之一。从语法上讲,它们与 React 中的状态管理非常相似。然而,信号的反应式能力赋予了它的诸多优势。比如下面的例子:
const [state, setState] = useState(0);
// state -> value
// setState -> setter
const [signal, setSignal] = createSignal(0);
// signal -> getter
// setSignal -> setter
看起来几乎相同,除了 useState 返回一个值而 useSignal 返回一个 getter 函数。
信号在其概念中相当于一个值的框,当一个框中的值发生变化时,所有相关框中的值都会自动更新。 Signal 会重复该过程,直到更新所有框。
乍一看,这非常类似于 useState 和 useEffect 的组合,但请注意信号是全局定义的。 这允许,当框中的值更改时,仅刷新 VirtualDOM 中依赖于它的那些组件。
import { signal, computed } from "@preact/signals";
const count = signal(0);
// computed
const double = computed(() => count.value * 2);
effect(() => console.log(double.value)));
function Counter() {
return (
<button onClick={() => count.value++}>
{count} x 2 = {double}
</button>
);
}
当然,这并不是 Signals 优势的全部。 在应用程序的整个生命周期中,信号引用不会改变,因此开发者不必担心多余的渲染。 computed 和 effect 函数都不需要依赖项列表而是会自动检测。 此外,如果依赖关系发生变化,但最终值保持不变,也不会刷新依赖关系。
本质上,Signals 是一个用纯 JavaScript 编写的库, 如果开发者在 JSX 中使用 signal,而不是 signal.value,它将被视为一个单独的组件。 这意味着如果它的值发生变化,不会渲染整个父组件,只会刷新一小段文本。
// 在此示例中,整个 Counter 组件将在计数更改时重新渲染
const count = signal(0);
function Counter() {
return (
<>
<SomeOtherComponent />
Value: {count.value}
</>
);
}
// 在此示例中,只有计数值会在计数更改时重新渲染
const count = signal(0);
function Counter() {
return (
<>
<SomeOtherComponent />
Value: {count}
</>
);
}
文章前面部分讲过,Signals 实际上是观察者模式的简单实现,当检索一个值时同时订阅了它。 当设置一个值时,实际上是发出一个事件。 真正的魔力始于 Signlas 和框架之间的接口。 为了方便开发人员,创建者应用了一些有争议的技巧,例如:重写 React.createElement 函数。
一旦 useState 返回一个值,库通常不再关心该值的使用。 开发人员必须自己决定在何处使用该值,并且必须确保想要订阅该值更改的任何 Effects、Memos 或 Callbacks 都在其依赖列表中提到该值。
图片来自:https://www.YouTube.com/watch?v=0t1tJTh0bLs
除此之外,记住该值以避免不必要的重新渲染,这显然对开发者提出了更高的要求。
比如下面的ParentComponent组件:
function ParentComponent() {
const [state, setState] = useState(0);
const stateVal = useMemo(() => {
return doSomeExpensiveStateCalculation(state);
}, [state]);
// 显式记忆并确保依赖关系准确
useEffect(() => {
sendDataToServer(state);
}, [state]);
// 显式调用状态订阅
return (
<div>
<ChildComponent stateVal={stateVal} />
</div>
);
}
createSignal 返回一个 getter 函数,因为信号本质上是反应性的。 为了进一步分解,信号跟踪谁对状态的变化感兴趣,如果发生变化,它会通知这些订阅者。
为了获得此订阅者信息,信号会跟踪调用这些状态获取器(本质上是一个函数)的上下文,调用 getter 创建订阅。这非常有用,因为库本身可以自行管理订阅状态更改的订阅者,并在更改后通知他们,而无需开发人员明确调用它。
createEffect(() => {
updateDataElswhere(state());
});
// effect 仅在 `state` 改变时运行 - 自动订阅
调用 getter 的上下文(不要与 React Context API 混淆)是库唯一会通知的上下文,这意味着记忆、显式填充大型依赖项数组以及修复不必要的重新渲染都可以有效避免。有助于避免使用大量用于此目的的额外 Hooks,例如 useRef、useCallback、useMemo 和大量重新渲染。
Signal 极大地增强了开发人员的体验,并将重点转移回为 UI 构建组件,而不是花费额外的 10% 的开发人员精力来遵守严格的性能优化语法规则。
function ParentComponent() {
const [state, setState] = createSignal(0);
const stateVal = doSomeExpensiveStateCalculation(state());
// 不需要显式记忆
createEffect(() => {
sendDataToServer(state());
});
// 只有在状态改变时才会被触发 - 效果会自动添加为订阅者
return (
<div>
<ChildComponent stateVal={stateVal} />
</div>
);
}
一般而言,使用信号和反应式编程似乎存在非常偏见的立场。 然而,事实并非如此。
React 是一个高性能、优化的库。尽管在以最佳方式使用状态方面存在一些差距或遗漏,从而导致不必要的重新渲染,但它仍然非常快。 在以某种方式使用 React 多年之后,前端开发人员已经习惯于将特定的数据流可视化并重新渲染,用反应式编程思维完全取代它在一定程度上显得跛脚。但是,React 仍然是构建用户界面的优秀选择。
反应式编程,除了性能增强之外,还通过归结为三个主要原语:Signal、Memo 和 Effects,使开发人员的体验更加简单。 这有助于开发者更多地关注为 UI 构建组件,而不是担心显式处理性能优化。
目前,Signal 也越来越受欢迎,并且是许多现代 Web 框架的一部分,例如 Solid.js、Preact、Qwik 和 Vue.js。
因为篇幅有限,文章并没有过多展开,如果有兴趣,可以在我的主页继续阅读,同时文末的参考资料提供了大量优秀文档以供学习。最后,欢迎大家点赞、评论、转发、收藏!
https://www.velotio.com/engineering-blog/why-signals-could-be-the-future-for-modern-web-frameworks
https://www.solidjs.com/guides/reactivity#how-it-works
https://www.builder.io/blog/usesignal-is-the-future-of-web-frameworks#what-signal-is
https://vived.io/signal-a-new-way-to-manage-Application-state-frontend-weekly-vol-104/
https://cloud.tencent.com/developer/article/1602301
https://blog.csdn.NET/wumu0927/article/detAIls/122288050
https://preactjs.com/guide/v10/signals/
https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8ob