Signals 在JavaScript中的应用

最近,”Signals”成为了前端备受关注的话题。很多国外的大佬都发文表示Signals是前端框架的未来。同时,尤大也在Vue官网上添加了”Connection to Signals”部分。此外,包括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)附加到视图树(view tree)某个特定部分的一个方法。可以做到的一个强大的事情是使其成为双向的。因此,我们可以让状态更新 DOM,反过来,DOM 事件自动更新状态,所有这些都是以一种简单的声明方式进行的。但是如果滥用也会出现问题,在 Angular 中,如果不知道有什么变化,就会对整个树进行肮脏的检查,向上传播可能会导致它发生多次。

1.3 Mobx

之后就是react的时代,react对状态管理没有太多的限制。MobX就是这种解决方案。它强调一致性和无障碍传播。也就是说,对于任何给定的变化,系统的每一部分都只运行一次,而且是以适当的顺序同步运行。

它通过将先前方案中典型的基于 push 的响应式换成 push-pull 混合系统来做到这一点。变化的通知被推送出去,但派生状态的执行被推迟到读取它的地方。

Signals 在JavaScript中的应用

1.4 Vue

Vue(2014) 也为今天的发展提供了巨大的贡献。除了在优化一致性方面与 MobX 保持一致外,Vue从一开始就将「细粒度」的响应性作为其核心。

虽然 Vue 与 React 共享虚拟 DOM 的使用,但响应性是一流的,这意味着它首先作为一种内部机制与框架一起开发,以支持其 Options API,并在过去几年中,成为 Composition API 的核心 (2020)。

Vue 通过调度任务,将 pull / push 机制向前推进了一步。默认情况下,Vue 的修改不会立马被执行,而是要等到下一个微任务才会执行。

然而,这种调度也可以用来做一些其他的事情,比如 keep-alive,以及 Suspense。甚至像并发渲染这样的事情也可以用这种方法来实现,真正展示了如何获得基于 pull 和 push 的两种方法的最佳效果。

二、为什么是Signals

Signals 的独特之处在于状态更改会以最有效的方式来自动更新组件和 UI。Signals 基于自动状态绑定和依赖跟踪提供了出色的工效,并具有针对虚拟 DOM 优化的独特实现。

2.1 状态管理的困境

随着应用越来越复杂,项目中的组件也会越来越多,需要管理的状态也越来越多。

为了实现组件状态共享,一般需要将状态提升到组件的共同的祖先组件里面,通过 props 往下传递,带来的问题就是更新时会导致所有子组件跟着更新,需要配合 memo 和 useMemo 来优化性能。

虽然这听起来还挺合理,但随着项目代码的增加,我们很难确定这些优化应该放到哪里。

即使添加了 memoization,也常常因为依赖值不稳定变得无效,由于 Hooks 没有可以用于分析的显式依赖关系树,所以也没法使用工具来找到原因。

Signals 在JavaScript中的应用

另一种解决方案就是放到 Context 上面,子组件作为消费者自行通过 useContext 来获取需要的状态。

但是有一个问题,只有传给 Provider 的值才能被更新,而且只能作为一个整体来更新,无法做到细粒度的更新。

为了处理这个问题,只能将 Context 进行拆分,业务逻辑又不可避免地会依赖多个 Context,这样就会出现 Context 套娃现象。

Signals 在JavaScript中的应用

2.2 通向未来的 Signals

Signal 的核心是一个通过.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 属性的组件。

Signals 在JavaScript中的应用

image

Signals 具有第二个重要特征,即它们会跟踪其值何时被访问以及何时被更新。在 Preact 中,当 Signal 的值发生变化时,从组件内访问 Signal 的属性会自动重新渲染组件。

通过Preact的使用,我们可以总结Signals 几点特点:1、感觉上像是使用原始数据结构 2、能根据值的变化自动更新 3、直接更新 DOM (换句话来说无 VDOM) 4、没有依赖数组

三、在SolidJS中的使用

const Greeting = (props) => (
  <>Hi <span>{props.name}</span></>
);

const App(() => {
  const [visible, setVisible] = createSignal(false),
    [name, setName] = createSignal("Josephine");

  return (
    <div onClick={() => setName("Geraldine")}>{
      visible() && <Greeting name={ name } />
    }</div>
  );
});

render(Appdocument.body);

可以看到 SolidJS 响应式也是Signal 作为基础,createSignal 既可以用于组件内,也可以用于组件外,这个跟 Preact 中类似。一方面可以将 Signal 作为组件的 local state,也可以定义为 global State。与前面类似,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的一些记录。希望能通过介绍响应式状态管理的一些历史和理念,让你对状态管理有全面的认识,如果感觉本文介绍的不够详细,可以阅读下面的引用原文。

六、引用

  1. dev.to/this-is-lea…
  2. dev.to/this-is-lea…
  3. mp.weixin.qq.com/s/Tn0rbkCdF…
  4. indepth.dev/posts/1289/…
  5. preactjs.com/guide/v10/s…
  6. preactjs.com/blog/introd…

原文链接:https://juejin.cn/post/7220769138101076026 作者:转转技术团队

(0)
上一篇 2023年4月12日 上午11:04
下一篇 2023年4月12日 上午11:14

相关推荐

发表回复

登录后才能评论