信号是响应式系统中的基础数据存储单元,用于存储可变状态。通过 `signal(initialValue)` 创建信号,返回一个 getter/setter 函数。调用该函数不带参数可读取当前值,带参数可写入新值。读取操作在响应式上下文中会自动追踪依赖,写入操作在值改变时会自动通知所有依赖该信号的订阅者。信号就像公告板,持有信息并在信息更新时通知所有关心它的人。
之前写了一篇文章关于 vue3.6 alien signals的简单介绍,接下来我们将深度分析源码关于其中的思想与实现
首先这是一个用于构建用户界面的响应式系统基础库。 它允许你创建响应式数据源(称为 信号 Signal),当这些数据源变化时,依赖它们的值(计算属性 Computed)和操作(副作用 Effect)会自动更新。 系统的核心引擎负责追踪依赖关系,并在数据变化时高效地调度和执行必要的更新,确保应用状态和视图保持同步。
接下来将会通过这几个部分深度解析这个源码
想象一下,你在开发一个简单的用户界面,需要显示用户的名字。这个名字可能会在用户登录后、或者在设置中被更改。当名字变化时,界面上所有显示这个名字的地方都应该自动更新。
如果用传统的方式,你可能需要手动找到所有显示名字的地方,然后逐一更新它们。这很繁琐,而且容易出错。如果能有一个“魔法盒子”来存放这个名字,当名字改变时,这个盒子能自动通知所有关心它变化的地方去更新,那该多好!
信号 (Signal) 就是这个“魔法盒子”。
在 alien signals
中,信号是响应式系统的基础数据存储单元。你可以:
用一个更形象的比喻:信号就像一个可以被监听的公告板。
信号使得数据变化能够自动、高效地传递给所有依赖它的部分,是构建复杂、动态应用的基础。
让我们通过一个简单的例子来看看如何创建和使用信号。
你可以使用 signal
函数来创建一个新的信号。你可以选择提供一个初始值,也可以不提供(此时初始值为 undefined
)。
import { signal } from './src/index.js'; // 导入 signal 函数
// 创建一个存储字符串的信号,初始值为 "初始名字"
const userName = signal("初始名字");
// 创建一个存储数字的信号,没有初始值
const userAge = signal<number>(); // 使用 <number> 指定类型,初始值为 undefined
解释:
signal("初始名字")
创建了一个信号,并将字符串 "初始名字"
作为它的初始值。signal<number>()
创建了一个信号,我们用 <number>
告诉 TypeScript 这个信号将来会存储数字类型,但现在它的初始值是 undefined
。signal
函数返回的不是信号的值本身,而是另一个函数。这个返回的函数非常特殊,它既可以用来读取信号的值,也可以用来写入新值。要读取信号当前的值,你需要调用创建信号时返回的那个函数,并且不带任何参数。
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
,因为我们创建它时没有给初始值。要更新信号的值,你需要调用创建信号时返回的那个函数,并且传入新的值作为参数。
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)
时:
currentValue
: 用来存储信号的当前值(也就是你传入的 initialValue
)。subs
: 一个列表(或者说链表),用来记录所有“关注”这个信号的订阅者 (Subscribers)。初始为空。subsTail
: 指向订阅者列表的末尾,用于快速添加新的订阅者。signal
函数会返回一个特殊的函数,我们称之为 getter/setter 函数。这个函数绑定 (bind) 了刚才创建的信号对象。这意味着当这个 getter/setter 函数被调用时,它内部的 this
会指向那个信号对象。// 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
函数:
activeSub
)来标记当前活动的订阅者。activeSub
不为 undefined
):这意味着这次读取操作是某个“订阅者”为了计算自己的值或执行某些操作而进行的。这时,系统需要建立信号和这个订阅者之间的依赖关系。它会调用 link(this, activeSub)
函数(this
指向信号对象,activeSub
指向当前活动的订阅者),将这个订阅者添加到信号对象的 subs
列表中(如果尚未添加的话)。这就像在公告板旁边记下:“某某订阅者正在关注这个公告板”。this.currentValue
。// 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)
来写入值时:
signalGetterSetter
函数,但这次传入了参数。newValue
和信号对象中存储的 this.currentValue
是否不同。this.currentValue = newValue
。subs
列表是否为空。如果不为空,说明有订阅者关注了这个信号。它会调用 propagate(this.subs)
函数(propagate
意为“传播”)。propagate
函数会遍历 subs
列表中的所有订阅者,并通知它们:“你关注的信号变化了,你可能需要重新计算或执行你的副作用了!” (propagate
的具体工作方式涉及标记订阅者为“脏”状态,我们将在 依赖追踪与链接 (Tracking & Linking) 和 响应式系统核心 (Reactive System Core) 章节深入探讨)。batchDepth
和 processEffectNotifications
的,它们用于批量处理更新,避免不必要的重复计算,我们稍后会接触)。// 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)
) 可以写入新值。信号就像响应式系统中的一个个小小的公告板,它们持有信息,并在信息更新时通知所有关心它们的人。