Alien Signals 技术分析之信号(Signal)(一)

8 分钟

AI 摘要 (由 qwen/qwen-2.5-72b-instruct 生成)

信号是响应式系统中的基础数据存储单元,用于存储可变状态。通过 `signal(initialValue)` 创建信号,返回一个 getter/setter 函数。调用该函数不带参数可读取当前值,带参数可写入新值。读取操作在响应式上下文中会自动追踪依赖,写入操作在值改变时会自动通知所有依赖该信号的订阅者。信号就像公告板,持有信息并在信息更新时通知所有关心它的人。

59.62s
~10383 tokens

引言

GitHub - stackblitz/alien-signals: 👾 The lightest signal library
👾 The lightest signal library. Contribute to stackblitz/alien-signals development by creating an account on GitHub.
网站图标github.com

之前写了一篇文章关于网站图标 vue3.6 alien signals的简单介绍,接下来我们将深度分析源码关于其中的思想与实现

首先这是一个用于构建用户界面的响应式系统基础库。 它允许你创建响应式数据源(称为 信号 Signal),当这些数据源变化时,依赖它们的值(计算属性 Computed)和操作(副作用 Effect)会自动更新。 系统的核心引擎负责追踪依赖关系,并在数据变化时高效地调度和执行必要的更新,确保应用状态和视图保持同步。

接下来将会通过这几个部分深度解析这个源码

什么是信号?为什么需要它?

想象一下,你在开发一个简单的用户界面,需要显示用户的名字。这个名字可能会在用户登录后、或者在设置中被更改。当名字变化时,界面上所有显示这个名字的地方都应该自动更新。

如果用传统的方式,你可能需要手动找到所有显示名字的地方,然后逐一更新它们。这很繁琐,而且容易出错。如果能有一个“魔法盒子”来存放这个名字,当名字改变时,这个盒子能自动通知所有关心它变化的地方去更新,那该多好!

信号 (Signal) 就是这个“魔法盒子”。

alien signals 中,信号是响应式系统的基础数据存储单元。你可以:

  1. 写入 (Write): 往信号里存入或更新一个值。
  2. 读取 (Read): 从信号里获取当前的值。
  3. 订阅 (Subscribe): 让其他部分(比如界面元素、或其他计算逻辑)“关注”这个信号。当信号的值发生变化时,所有关注它的“订阅者”都会收到通知。

用一个更形象的比喻:信号就像一个可以被监听的公告板

  • 写入:你在公告板上更新内容。
  • 读取:你去看公告板上的内容。
  • 订阅:如果你在看公告板的时候,表明了你“关心”这个公告板的更新(比如你是一个需要根据公告板信息做事的“订阅者”),那么你的名字就会被记录下来。下次公告板更新时,你就会收到通知。

信号使得数据变化能够自动、高效地传递给所有依赖它的部分,是构建复杂、动态应用的基础。

如何使用信号?

让我们通过一个简单的例子来看看如何创建和使用信号。

1. 创建信号

你可以使用 signal 函数来创建一个新的信号。你可以选择提供一个初始值,也可以不提供(此时初始值为 undefined)。

typescript
import { signal } from './src/index.js'; // 导入 signal 函数

// 创建一个存储字符串的信号,初始值为 "初始名字"
const userName = signal("初始名字");

// 创建一个存储数字的信号,没有初始值
const userAge = signal<number>(); // 使用 <number> 指定类型,初始值为 undefined

解释:

  • signal("初始名字") 创建了一个信号,并将字符串 "初始名字" 作为它的初始值。
  • signal<number>() 创建了一个信号,我们用 <number> 告诉 TypeScript 这个信号将来会存储数字类型,但现在它的初始值是 undefined
  • signal 函数返回的不是信号的值本身,而是另一个函数。这个返回的函数非常特殊,它既可以用来读取信号的值,也可以用来写入新值。

2. 读取信号的值

要读取信号当前的值,你需要调用创建信号时返回的那个函数,并且不带任何参数

typescript
import { signal } from './src/index.js';

const userName = signal("张三");

// 读取 userName 信号的值
const currentName = userName(); // 调用返回的函数,不传参数

console.log(currentName); // 输出: 张三

const userAge = signal<number>();
const currentAge = userAge();

console.log(currentAge); // 输出: undefined

解释:

  • 调用 userName()(注意后面的括号 ())会返回信号 userName 当前存储的值 "张三"
  • 调用 userAge() 会返回 undefined,因为我们创建它时没有给初始值。

3. 写入信号的值

要更新信号的值,你需要调用创建信号时返回的那个函数,并且传入新的值作为参数

typescript
import { signal } from './src/index.js';

const userName = signal("张三");

console.log("更新前:", userName()); // 输出: 更新前: 张三

// 写入新值 "李四" 到 userName 信号
userName("李四"); // 调用返回的函数,传入新值

console.log("更新后:", userName()); // 输出: 更新后: 李四

解释:

  • 调用 userName("李四") 将信号 userName 内部存储的值更新为 "李四"
  • 再次调用 userName() 读取时,得到的就是新值 "李四"

总结一下:

signal(initialValue) 返回一个函数 s

  • 调用 s():读取信号的值。
  • 调用 s(newValue):写入信号的值。

这种设计可能看起来有点奇怪,但它为响应式系统提供了一种统一且简洁的交互方式。

信号的内部机制(初步了解)

现在我们知道了如何使用信号,让我们稍微深入一点,看看它内部是如何工作的。这有助于我们理解为什么它能实现“自动通知”。

创建时发生了什么?

当你调用 signal(initialValue) 时:

  1. 内部会创建一个信号对象 (Signal Object)。这个对象至少包含:
    • currentValue: 用来存储信号的当前值(也就是你传入的 initialValue)。
    • subs: 一个列表(或者说链表),用来记录所有“关注”这个信号的订阅者 (Subscribers)。初始为空。
    • subsTail: 指向订阅者列表的末尾,用于快速添加新的订阅者。
  2. signal 函数会返回一个特殊的函数,我们称之为 getter/setter 函数。这个函数绑定 (bind) 了刚才创建的信号对象。这意味着当这个 getter/setter 函数被调用时,它内部的 this 会指向那个信号对象。
src/index.ts (简化示意)
typescript
// src/index.ts (简化示意)
function signal<T>(initialValue?: T) {
  // 1. 创建信号对象
  const signalObject = {
    currentValue: initialValue,
    subs: undefined,       // 初始没有订阅者
    subsTail: undefined,   // 初始没有订阅者
  };

  // 2. 返回绑定了信号对象的 getter/setter 函数
  return signalGetterSetter.bind(signalObject) as /* ...类型 */;
}

读取时发生了什么? (signalGetterSetter 无参数调用)

当你调用 theSignal() 来读取值时,实际上是在调用那个绑定了信号对象的 signalGetterSetter 函数:

  1. 函数检查当前是否在一个响应式上下文 (Reactive Context) 中。响应式上下文通常由 计算属性 (Computed) 或 副作用 (Effect) 创建(我们将在后续章节学习它们)。系统内部会有一个全局变量(比如 activeSub)来标记当前活动的订阅者。
  2. 如果存在活动的订阅者 (activeSub 不为 undefined):这意味着这次读取操作是某个“订阅者”为了计算自己的值或执行某些操作而进行的。这时,系统需要建立信号和这个订阅者之间的依赖关系。它会调用 link(this, activeSub) 函数(this 指向信号对象,activeSub 指向当前活动的订阅者),将这个订阅者添加到信号对象的 subs 列表中(如果尚未添加的话)。这就像在公告板旁边记下:“某某订阅者正在关注这个公告板”。
  3. 无论是否存在活动的订阅者,函数最后都会返回信号对象中存储的 this.currentValue
src/index.ts (简化示意 - signalGetterSetter 读取部分)
typescript
// src/index.ts (简化示意 - signalGetterSetter 读取部分)
function signalGetterSetter<T>(this: Signal<T>) {
  // (这里省略了写入逻辑的判断)

  // 检查是否有活动的订阅者
  if (activeSub !== undefined) {
    // 如果有,建立依赖关系:将 activeSub 添加到 this (信号对象) 的订阅者列表 subs 中
    link(this, activeSub); // `link` 来自响应式系统核心
  }
  // 返回当前值
  return this.currentValue;
}

写入时发生了什么? (signalGetterSetter 有参数调用)

当你调用 theSignal(newValue) 来写入值时:

  1. 同样是调用 signalGetterSetter 函数,但这次传入了参数。
  2. 函数首先比较传入的 newValue 和信号对象中存储的 this.currentValue 是否不同
  3. 如果值确实发生了变化:
    • 更新 this.currentValue = newValue
    • 通知订阅者! 这是关键步骤。函数会检查信号对象的 subs 列表是否为空。如果不为空,说明有订阅者关注了这个信号。它会调用 propagate(this.subs) 函数(propagate 意为“传播”)。propagate 函数会遍历 subs 列表中的所有订阅者,并通知它们:“你关注的信号变化了,你可能需要重新计算或执行你的副作用了!” (propagate 的具体工作方式涉及标记订阅者为“脏”状态,我们将在 依赖追踪与链接 (Tracking & Linking) 和 响应式系统核心 (Reactive System Core) 章节深入探讨)。
    • (还有一个细节是关于 batchDepthprocessEffectNotifications 的,它们用于批量处理更新,避免不必要的重复计算,我们稍后会接触)。
  4. 如果值没有变化,则什么也不做。
src/index.ts (简化示意 - signalGetterSetter 写入部分)
typescript
// src/index.ts (简化示意 - signalGetterSetter 写入部分)
function signalGetterSetter<T>(this: Signal<T>, ...value: [T]): T | void {
  if (value.length) { // 检查是否传入了参数 (即写入操作)
    const newValue = value[0];
    // 检查值是否真的改变了
    if (this.currentValue !== newValue) {
      // 1. 更新当前值
      this.currentValue = newValue;
      // 2. 获取订阅者列表
      const subs = this.subs;
      if (subs !== undefined) {
        // 3. 如果有订阅者,通知它们 (传播更新)
        propagate(subs); // `propagate` 来自响应式系统核心
        // (处理批量更新,暂时忽略)
        // if (!batchDepth) {
        //   processEffectNotifications();
        // }
      }
    }
  } else {
    // ... 读取逻辑 (如上所述) ...
  }
}


简单流程图

让我们用一个简单的图来梳理一下读取和写入的流程:

关键点:

  • 信号本身只是存储数据 (currentValue) 和订阅者列表 (subs)。
  • 读取操作可能会建立依赖关系(通过 link)。
  • 写入操作在值改变时会触发通知(通过 propagate)。

这就是信号实现其“魔法”——自动通知变化——的基本原理。它构成了整个响应式系统的基石。

总结

在本章中,我们学习了 alien signals 响应式系统的核心概念——信号 (Signal)

  • 信号是用于存储可变状态的基础单元。
  • 使用 signal(initialValue) 创建信号,它返回一个特殊的getter/setter 函数
  • 调用返回的函数不带参数 (signal()) 可以读取当前值。
  • 调用返回的函数带参数 (signal(newValue)) 可以写入新值。
  • 读取操作在响应式上下文中会自动追踪依赖(将当前订阅者链接到信号)。
  • 写入操作在值改变时会自动通知所有依赖该信号的订阅者。

信号就像响应式系统中的一个个小小的公告板,它们持有信息,并在信息更新时通知所有关心它们的人。