|
最近,"Signals"成为了前端备受关注的话题。很多国外的大佬都发文表示Signals是前端框架的未来。同时,尤大也在Vue官网上添加了"ConnectiontoSignals"部分。此外,包括Solid、Angular、Preact、Qwik和Vue等多个前端框架都已经开始实现Signals。作为一名FE,如果你和我之前一样还不是很了解Signals,那么这篇文章或许可以帮助你更好地了解一下这个技术。本文将介绍Signals的历史、概念和优势。一、发展历史自从声明式JavaScript框架问世以来,Signals机制一直存在。随着时间的推移,它采用了许多不同的名称,经历了多年的流行和消失。在声明式JavaScript框架中,组件是声明其输出的单元,可以被动态地渲染和组合。这种方式的优点是,它允许开发人员集中精力于组件的输出,而无需担心组件如何被渲染和更新。这种抽象方式也使得组件更容易被复用,并且更容易理解和测试。然而,这种抽象也带来了一些挑战。其中一个挑战是组件之间如何通信和共享状态。这些问题可能导致代码变得笨重,难以维护,并且在复杂的应用程序中容易出现混乱。因此,开发人员需要一种更灵活、更强大的通信机制来解决这些问题。在这种情况下,Signals机制成为了一个有用的解决方案。Signals机制允许组件在不直接引用其他组件的情况下通信,并且能够更灵活地传递消息和状态。这些机制可以是事件、回调、Promise或其他异步机制。它们可以被用来处理各种不同的场景,例如用户交互、网络请求和状态更改等。Signals机制还具有许多其他优点。它们可以提高应用程序的可维护性和可扩展性,并且可以帮助开发人员更好地理解和调试代码。此外,由于Signals机制允许组件之间松散耦合,因此它们也有助于提高代码的可重用性。1.1早期实现有时令人惊讶的是,多个团队几乎在同一时间达成了相似的解决方案。声明式JavaScript框架的开端有三个版本:Knockout.js(2010年7月),Backbone.js(2010年10月)和Angular.js(2010年10月)。Angular的脏检查,Backbone的模型驱动的重渲染,以及Knockout的细粒度更新。每一个都略有不同,但最终都将成为我们今天管理状态和更新DOM的基础。1.2数据绑定Angular.js里面常用的模式叫作数据绑定。数据绑定是将部分状态(state)附加到视图树(viewtree)某个特定部分的一个方法。可以做到的一个强大的事情是使其成为双向的。因此,我们可以让状态更新DOM,反过来,DOM事件自动更新状态,所有这些都是以一种简单的声明方式进行的。但是如果滥用也会出现问题,在Angular中,如果不知道有什么变化,就会对整个树进行肮脏的检查,向上传播可能会导致它发生多次。1.3Mobx之后就是react的时代,react对状态管理没有太多的限制。MobX就是这种解决方案。它强调一致性和无障碍传播。也就是说,对于任何给定的变化,系统的每一部分都只运行一次,而且是以适当的顺序同步运行。它通过将先前方案中典型的基于push的响应式换成push-pull混合系统来做到这一点。变化的通知被推送出去,但派生状态的执行被推迟到读取它的地方。image1.4VueVue(2014)也为今天的发展提供了巨大的贡献。除了在优化一致性方面与MobX保持一致外,Vue从一开始就将「细粒度」的响应性作为其核心。虽然Vue与React共享虚拟DOM的使用,但响应性是一流的,这意味着它首先作为一种内部机制与框架一起开发,以支持其OptionsAPI,并在过去几年中,成为CompositionAPI的核心(2020)。Vue通过调度任务,将pull/push机制向前推进了一步。默认情况下,Vue的修改不会立马被执行,而是要等到下一个微任务才会执行。然而,这种调度也可以用来做一些其他的事情,比如keep-alive,以及Suspense。甚至像并发渲染这样的事情也可以用这种方法来实现,真正展示了如何获得基于pull和push的两种方法的最佳效果。二、为什么是SignalsSignals的独特之处在于状态更改会以最有效的方式来自动更新组件和UI。Signals基于自动状态绑定和依赖跟踪提供了出色的工效,并具有针对虚拟DOM优化的独特实现。2.1状态管理的困境随着应用越来越复杂,项目中的组件也会越来越多,需要管理的状态也越来越多。为了实现组件状态共享,一般需要将状态提升到组件的共同的祖先组件里面,通过props往下传递,带来的问题就是更新时会导致所有子组件跟着更新,需要配合memo和useMemo来优化性能。虽然这听起来还挺合理,但随着项目代码的增加,我们很难确定这些优化应该放到哪里。即使添加了memoization,也常常因为依赖值不稳定变得无效,由于Hooks没有可以用于分析的显式依赖关系树,所以也没法使用工具来找到原因。image另一种解决方案就是放到Context上面,子组件作为消费者自行通过useContext来获取需要的状态。但是有一个问题,只有传给Provider的值才能被更新,而且只能作为一个整体来更新,无法做到细粒度的更新。为了处理这个问题,只能将Context进行拆分,业务逻辑又不可避免地会依赖多个Context,这样就会出现Context套娃现象。image2.2通向未来的SignalsSignal的核心是一个通过.value属性来保存值的对象。它有一个重要特征,那就是Signal对象的值可以改变,但Signal本身始终保持不变。import { signal } from "@preact/signals";const count = signal(0);// Read a signal’s value by accessing .value:console.log(count.value); // 0// Update a signal’s value:count.value += 1;// The signal's value has changed:console.log(count.value); // 1在Preact中,当Signal作为props或context向下传递时,传递的是对Signal的引用。这样就可以在不重新渲染组件的情况下更新Signal,因为传给组件的是Signal对象而不是它的值。这让我们可以跳过所有昂贵的渲染工作,立即跳到任意访问signal.value属性的组件。imageSignals具有第二个重要特征,即它们会跟踪其值何时被访问以及何时被更新。在Preact中,当Signal的值发生变化时,从组件内访问Signal的属性会自动重新渲染组件。通过Preact的使用,我们可以总结Signals几点特点:1、感觉上像是使用原始数据结构2、能根据值的变化自动更新3、直接更新DOM(换句话来说无VDOM)4、没有依赖数组三、在SolidJS中的使用const Greeting = (props) => ( Hi {props.name});const App(() => { const [visible, setVisible] = createSignal(false), [name, setName] = createSignal("Josephine"); return ( setName("Geraldine")}>{ visible() & } );});render(App, document.body);可以看到SolidJS响应式也是Signal作为基础,createSignal既可以用于组件内,也可以用于组件外,这个跟Preact中类似。一方面可以将Signal作为组件的localstate,也可以定义为globalState。与前面类似,SolidJS中也有以下相似点:响应式细粒度更新无需定义dependencies惰性取值SolidJS与Mobx和Vue的响应式非常相似,但是不会处理VDOM,而是直接更新DOM。四、手动实现一个响应式状态管理三要素,Signals、Reactions、Derivations// context 包含Reactions中的执行方法和Signal依赖const context = [];const createSignal = (value) => { const subscriptions = new Set(); const readFn = () => { const running = context.pop(); if (running) { subscriptions.add({ execute: running.execute }); running.deps.add(subscriptions); } return value; }; const writeFn = (newValue) => { value = newValue; for (const sub of [...subscriptions]) { sub.execute(); } }; return [readFn, writeFn];};const createEffect = (fn) => { const execute = () => { running.deps.clear(); context.push(running); try { fn(); } finally { context.pop(running); } }; const running = { execute, deps: new Set() }; execute();};const createMemo = (fn) => { const [memo, setMemo] = createSignal(); createEffect(() => setMemo(fn())); return memo;};const [name, setName] = createSignal("a");const fullName = createMemo(() => { return "c-" + name();});createEffect(() => console.log(name(), fullName()));setName("b");Signals是一个基础的数据更新与读取,Reactions是可以追踪订阅到Signals的变化,所以在Reactions函数里设置Derivations的值。五、最后本文是学习Signals的一些记录。希望能通过介绍响应式状态管理的一些历史和理念,让你对状态管理有全面的认识,如果感觉本文介绍的不够详细,可以阅读下面的引用原文。六、引用https://dev.to/this-is-learning/the-evolution-of-signals-in-javascript-8obhttps://dev.to/this-is-learning/react-vs-signals-10-years-later-3k71https://mp.weixin.qq.com/s/Tn0rbkCdFw4f-3ihKUEYQAhttps://indepth.dev/posts/1289/solidjs-reactivity-to-renderinghttps://preactjs.com/guide/v10/signals/https://preactjs.com/blog/introducing-signals
|
|