本文介绍了响应式系统中的副作用(Effect)机制,它能够自动监听信号(Signal)和计算属性(Computed)的变化并执行指定操作。Effect通过依赖追踪实现自动化响应,在创建时立即执行一次并记录依赖项,当依赖变化时自动重新运行。文章详细说明了Effect的创建、执行和停止方法,以及其内部工作机制,包括依赖收集和通知传播流程。Effect是连接响应式数据与实际应用行为的关键,与Signal和Computed共同构成了响应式编程的基础。
在前面的章节中,我们学习了 信号 (Signal) 如何存储基本状态,以及 计算属性 (Computed) 如何根据现有状态派生出新的状态。我们现在拥有了描述和管理应用数据的工具。但是,光有数据还不够,我们通常希望当数据变化时,能够自动执行某些操作,比如更新用户界面、在控制台打印日志、或者向服务器发送请求。
这就是 副作用 (Effect) 发挥作用的地方!
想象一下,我们有一个简单的计数器应用。我们用一个信号 (Signal) 来存储当前的计数值。
import { signal } from './src/index.js';
const counter = signal(0);
// 我们想在每次 counter 变化时,都在控制台打印出新的值
// 如果手动操作:
console.log("当前计数值:", counter()); // 输出: 当前计数值: 0
counter(1); // 增加计数
// 需要手动再次打印!
console.log("当前计数值:", counter()); // 输出: 当前计数值: 1
counter(2); // 再次增加计数
// 又要手动打印!
console.log("当前计数值:", counter()); // 输出: 当前计数值: 2
和我们在计算属性 (Computed) 中遇到的问题类似,手动在每次数据变化后去执行相应的操作是非常繁琐和容易出错的。我们希望有一种方法能够自动监听数据的变化,并在变化发生时自动执行我们指定的操作。
副作用 (Effect) 就是实现这种自动响应的机制。
你可以把它想象成一个自动化的警报系统:
副作用通常用于执行那些与响应式系统状态相关的外部操作(即不直接产生新状态,而是影响系统外部环境的操作),例如:
使用 effect
函数可以创建一个副作用。你需要传递给它一个副作用函数 (effect function),这个函数包含了你希望在依赖项变化时执行的操作。
让我们用 effect
来自动化上面例子中的日志打印:
import { signal, effect } from './src/index.js'; // 导入 effect 函数
const counter = signal(0);
// 创建一个副作用来监听 counter 的变化
effect(() => {
// 这个函数就是副作用逻辑
const currentValue = counter(); // 在函数内部读取 counter 信号
console.log("Effect 侦测到变化,当前计数值:", currentValue);
});
console.log("副作用已创建并立即执行了一次。");
输出:
Effect 侦测到变化,当前计数值: 0
副作用已创建并立即执行了一次。
解释:
effect
并传入一个箭头函数 () => { ... }
。counter
信号的值 (counter()
)。master
的响应式系统会自动侦测到这次读取,并将 counter
信号标记为这个 effect
的依赖项。effect
在创建时会立即执行一次其内部的函数。这就是为什么我们马上看到了第一条日志输出 "Effect 侦测到变化,当前计数值: 0"。这确保了你的副作用逻辑能基于初始状态运行一次。现在,让我们看看当 counter
信号变化时会发生什么:
// (接上例)
console.log("准备更新 counter...");
counter(1); // 更新 counter 信号的值
console.log("counter 已更新,触发 effect 重新执行。");
counter(2); // 再次更新 counter
console.log("counter 再次更新,再次触发 effect。");
输出:
// ...之前的输出...
准备更新 counter...
Effect 侦测到变化,当前计数值: 1 // effect 自动重新执行了!
counter 已更新,触发 effect 重新执行。
Effect 侦测到变化,当前计数值: 2 // effect 又自动重新执行了!
counter 再次更新,再次触发 effect。
解释:
counter(1)
时,counter
信号 (Signal) 的值发生了变化。effect
在首次运行时读取了 counter
,响应式系统已经知道这个 effect
依赖于 counter
。counter
更新时,系统会自动通知这个 effect
:“你的依赖项变了!”effect
的那个函数 () => { ... }
。这就是为什么控制台打印了 "Effect 侦测到变化,当前计数值: 1"。counter(2)
时,这个过程会再次发生,导致函数又一次执行,打印出 "Effect 侦测到变化,当前计数值: 2"。总结一下 effect
的行为:
effect(fn)
创建一个副作用。fn
会立即执行一次。fn
执行期间,所有被读取的信号 (Signal) 或 计算属性 (Computed) 都会被自动追踪为该 effect
的依赖项。fn
会被自动重新执行。有时你可能希望停止一个副作用,让它不再响应依赖项的变化。effect
函数会返回一个停止函数 (stop function)。调用这个函数就可以停止对应的副作用。
import { signal, effect } from './src/index.js';
const counter = signal(10);
// 创建 effect 并获取停止函数
const stopEffect = effect(() => {
console.log("Effect 运行中,计数值:", counter());
});
console.log("--- 第一次更新 ---");
counter(11); // Effect 会执行
console.log("--- 停止 Effect ---");
stopEffect(); // 调用停止函数
console.log("--- 第二次更新 ---");
counter(12); // Effect 不再执行
console.log("Effect 已停止。");
输出:
Effect 运行中,计数值: 10 // 首次执行
--- 第一次更新 ---
Effect 运行中,计数值: 11 // 因 counter(11) 而执行
--- 停止 Effect ---
--- 第二次更新 ---
Effect 已停止。 // counter(12) 后,Effect 没有再打印日志
解释:
effect(...)
返回了一个名为 stopEffect
的函数。stopEffect()
之前,effect
正常工作,每次 counter
变化都会重新运行。stopEffect()
之后,这个 effect
就被“清理”了。它与它的依赖项(counter
)之间的连接被断开。counter(12)
执行时,虽然 counter
的值变了,但之前那个 effect
不再收到通知,也不会再执行了。这对于在组件卸载或特定条件满足时清理不再需要的自动行为非常有用。
理解副作用的内部工作原理能帮助我们更好地认识响应式系统。
effect
函数)当你调用 effect(fn)
时:
fn
: 你传入的副作用函数。flags
: 一些状态标记,比如标记自己是副作用 (SubscriberFlags.Effect
)。deps
: 一个列表(链表),用来记录这个副作用依赖于哪些信号或计算属性。初始为空。subs
: 副作用通常不作为其他计算的依赖(它在依赖链的末端),所以这个可能为空或不被主要使用。EffectScope
相关联,我们将在后面的章节 副作用作用域 (EffectScope) 讨论)。computed
最大的不同之处。effect
不会等待被读取,而是立即开始第一次执行:startTracking(this)
。设置全局变量 activeSub = this
(this
指向新创建的副作用对象)。fn
: 调用用户提供的 this.fn()
函数。fn
函数执行期间,如果它读取了某个信号 (Signal)(比如 counter()
),信号的读取逻辑会检测到 activeSub
(即当前副作用对象),并调用 link(signalObject, this)
,建立信号与副作用之间的依赖关系(将副作用添加到信号的 subs
列表,将信号添加到副作用的 deps
列表)。endTracking(this)
。清除 activeSub
,完成依赖追踪。effectStop
): 这个函数绑定了刚才创建的副作用对象,调用它时可以清理依赖关系。// src/index.ts (简化示意)
export function effect<T>(fn: () => T): () => void {
// 1. 创建 Effect 对象
const e: Effect = {
fn,
subs: undefined,
subsTail: undefined,
deps: undefined,
depsTail: undefined,
flags: SubscriberFlags.Effect, // 标记为 Effect
};
// (如果当前在 EffectScope 内,建立链接,暂时忽略)
// if (activeScope !== undefined) { link(e, activeScope); }
// 2. 立即执行并追踪依赖
const prevSub = activeSub;
activeSub = e; // 将当前 effect 设为活动订阅者
startTracking(e); // 准备追踪依赖
try {
e.fn(); // 执行用户函数,期间读取的信号会自动 link 到 e
} finally {
endTracking(e); // 完成追踪,清理未使用的依赖
activeSub = prevSub; // 恢复之前的活动订阅者
}
// 3. 返回绑定了 Effect 对象的停止函数
return effectStop.bind(e);
}
// src/index.ts (简化示意 - effectStop)
function effectStop(this: Subscriber): void {
// startTracking 和 endTracking 配合可以清理所有依赖
startTracking(this);
endTracking(this);
// (可能还需要将 effect 从其 scope 中移除等清理操作)
}
当副作用依赖的某个信号 (Signal)(比如 counter
)的值发生变化时:
signalGetterSetter
带参数调用) 会执行。subs
)。因为在 effect
第一次执行时,counter
通过 link
函数把这个 effect
加入了自己的 subs
列表。propagate(this.subs)
来通知它的所有订阅者。propagate
函数会遍历订阅者列表,找到对应的 effect
对象。effect
对象添加一个标记(例如 Notified
或 PendingEffect
),表明这个副作用需要被处理。propagate
可能会将这个 effect
对象添加到一个待处理的通知缓冲区 (notifyBuffer
) 中。batchDepth === 0
),信号的写入逻辑会调用 processEffectNotifications()
。processEffectNotifications()
会遍历通知缓冲区,找到被标记的 effect
。effect
,它会调用 notifyEffect(e)
。notifyEffect
内部(或者它调用的函数):activeSub = e
并调用 startTracking(e)
。e.fn()
。endTracking(e)
并恢复 activeSub
。这样,依赖项的变化就触发了副作用函数的重新执行。
关键点:
effect
在创建时立即执行一次,并在此期间收集依赖。effect
被标记并排队等待执行(通过 propagate
和 notifyBuffer
)。processEffectNotifications
和 notifyEffect
)负责重新执行被标记的 effect
函数。effect
函数时,也会进行依赖追踪,允许依赖关系动态变化。effect
返回的函数可以用来停止副作用。在本章中,我们学习了副作用 (Effect),它是响应式系统中用于执行操作的重要部分:
effect(fn)
创建副作用,fn
会在创建时立即执行一次。effect
函数返回一个停止函数,可以用来手动停止副作用的自动执行和依赖追踪。副作用将我们的响应式数据与现实世界连接起来,让数据的变化能够驱动应用的实际行为。
至此,我们已经学习了 master
响应式系统的三个核心用户 API:
这三者共同构成了响应式编程的基础。但是,它们是如何协同工作的?是什么在幕后默默地进行依赖追踪、标记更新和触发执行?