Learning Record
目录
响应式系统概览
Vue3 的响应式系统基于 依赖追踪 和 发布-订阅 模式,核心思想是:
- 响应式数据(Ref/Reactive):数据变化时自动通知依赖
- Effect:副作用函数,访问响应式数据时自动建立依赖关系
- 双向链表:高效管理 Dependency(依赖项)和 Subscriber(订阅者)的关系
核心流程
1. 创建响应式数据 (ref/reactive)
2. 在 effect 中访问响应式数据
3. 建立依赖关系(依赖收集)
4. 数据变化时自动触发 effect 重新执行(派发更新)
Ref 的实现原理
1. RefImpl 类的结构
class RefImpl implements Dependency {
_value; // 保存实际的值
[ReactiveFlags.IS_REF] = true; // ref 标记
subs: Link; // 订阅者链表的头节点
subsTail: Link; // 订阅者链表的尾节点
constructor(value) {
// 如果 value 是对象,使用 reactive 转换为响应式
this._value = isObject(value) ? reactive(value) : value;
}
}
2. 核心访问器
getter - 依赖收集
get value() {
// 如果有当前正在执行的 effect,收集依赖
if (activeSub) {
trackRef(this);
}
return this._value;
}
关键点:
- 只有在
activeSub存在时才收集依赖 activeSub是当前正在执行的 effect 实例
setter - 派发更新
set value(newValue) {
if (hasChanged(newValue, this._value)) {
// 只有值真正改变时才触发更新
this._value = isObject(newValue) ? reactive(newValue) : newValue;
triggerRef(this); // 触发依赖的 effect 重新执行
}
}
关键点:
- 使用
hasChanged判断新旧值是否不同 - 避免不必要的更新,提升性能
3. 依赖收集和触发
// 收集依赖
function trackRef(dep) {
if (activeSub) {
link(dep, activeSub); // 建立双向链表关系
}
}
// 触发更新
function triggerRef(dep) {
if (dep.subs) {
propagate(dep.subs); // 从头节点开始遍历,通知所有订阅者
}
}
Effect 的基础实现
1. ReactiveEffect 类
class ReactiveEffect implements Sub {
active = true; // 表示 effect 是否激活
deps: Link; // 依赖项链表的头节点
depsTail: Link; // 依赖项链表的尾节点
tracking = false; // 标记是否正在追踪依赖
dirty = false; // 标记是否需要重新执行
constructor(public fn) {}
}
2. run 方法 - Effect 执行核心
run() {
// 如果 effect 已经被停止,直接执行函数不收集依赖
if (!this.active) {
return this.fn();
}
// 保存之前的 effect(处理嵌套 effect)
const prevSub = activeSub;
// 设置当前 effect 为活动状态
setActiveSub(this);
startTrack(this); // 开始追踪依赖
try {
return this.fn(); // 执行用户函数,期间会触发响应式数据的 getter
} finally {
endTrack(this); // 结束追踪,清理过期依赖
setActiveSub(prevSub); // 恢复之前的 effect
}
}
关键设计:
- 嵌套 effect 支持:通过保存和恢复
prevSub实现 - 依赖追踪范围:只在
startTrack和endTrack之间收集依赖 - 自动清理:
endTrack会清理不再需要的旧依赖
3. 通知和调度
// 通知方法:依赖数据变化时被调用
notify() {
this.scheduler(); // 委托给调度器
}
// 调度器:默认直接执行,可自定义
scheduler() {
this.run(); // 默认行为:直接重新执行
}
4. stop 方法 - 停止 Effect
stop() {
if (this.active) {
startTrack(this); // 开始追踪
endTrack(this); // 立即结束,触发依赖清理
this.active = false; // 标记为不活跃
}
}
5. effect 函数 - 创建 Effect
function effect(fn, options) {
const e = new ReactiveEffect(fn);
// 合并用户选项(如 scheduler)
Object.assign(e, options);
// 立即执行一次,收集依赖
e.run();
// 创建 runner 函数
const runner = e.run.bind(e);
// 将 effect 实例挂载到 runner 上,方便外部访问
runner.effect = e;
return runner;
}
为什么要定义 runner 变量?
不能写成 return () => e.run(),因为需要:
- 在返回的函数上挂载
effect属性 - 让用户能通过
runner.effect访问原始 effect 实例 - 例如:
runner.effect.stop()停止 effect
const runner = effect(() => {
console.log("effect");
});
// 通过 runner 访问 effect 实例
runner.effect.stop(); // 停止 effect
依赖收集系统
1. 双向链表结构
Vue3 使用 双向链表 来管理依赖关系,这是一个核心设计:
interface Dependency {
subs: Link; // 订阅者链表头节点
subsTail: Link; // 订阅者链表尾节点
}
interface Sub {
deps: Link; // 依赖项链表头节点
depsTail: Link; // 依赖项链表尾节点
tracking: boolean;
}
interface Link {
sub: Sub; // 订阅者
dep: Dependency; // 依赖项
nextSub: Link; // 下一个订阅者
prevSub: Link; // 上一个订阅者
nextDep: Link; // 下一个依赖项
}
结构示意:
Dependency (ref/reactive 的属性)
↓ subs
Link1 → Link2 → Link3 (订阅者链表)
↓ ↓ ↓
Sub1 Sub2 Sub3 (effect 实例)
Subscriber (effect 实例)
↓ deps
Link1 → Link2 → Link3 (依赖项链表)
↓ ↓ ↓
Dep1 Dep2 Dep3 (响应式数据)
2. link 函数 - 建立链表关系
function link(dep, sub) {
// 1. 尝试复用已有的链表节点
const currentDep = sub.depsTail;
const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep;
if (nextDep && nextDep.dep === dep) {
// 依赖未变化,直接复用
sub.depsTail = nextDep;
return;
}
// 2. 创建或复用节点
let newLink;
if (linkPool) {
// 从对象池复用节点
newLink = linkPool;
linkPool = linkPool.nextDep;
newLink.nextDep = nextDep;
newLink.dep = dep;
newLink.sub = sub;
} else {
// 创建新节点
newLink = { sub, dep, nextDep, nextSub: undefined, prevSub: undefined };
}
// 3. 将节点链接到 dep 的订阅者链表
if (dep.subsTail) {
dep.subsTail.nextSub = newLink;
newLink.prevSub = dep.subsTail;
dep.subsTail = newLink;
} else {
dep.subs = newLink;
dep.subsTail = newLink;
}
// 4. 将节点链接到 sub 的依赖项链表
if (sub.depsTail) {
sub.depsTail.nextDep = newLink;
sub.depsTail = newLink;
} else {
sub.deps = newLink;
sub.depsTail = newLink;
}
}
优化亮点:
- 节点复用:通过
linkPool对象池复用节点,减少 GC 压力 - 双向关联:同时建立 dep→sub 和 sub→dep 的关系
- 尾节点优化:通过
depsTail和subsTail实现 O(1) 的尾部插入
3. startTrack - 开始追踪
function startTrack(sub) {
sub.tracking = true;
sub.depsTail = undefined; // 重置尾节点,用于后续判断哪些依赖是新的
}
作用:
- 标记开始收集依赖
- 重置
depsTail,用于区分新旧依赖
4. endTrack - 结束追踪并清理
function endTrack(sub) {
sub.tracking = false;
const depsTail = sub.depsTail;
sub.dirty = false;
// 清理不再需要的旧依赖
if (depsTail) {
if (depsTail.nextDep) {
clearTracking(depsTail.nextDep);
depsTail.nextDep = undefined;
}
} else if (sub.deps) {
clearTracking(sub.deps);
sub.deps = undefined;
}
}
清理逻辑:
depsTail之后的节点是旧依赖(本次执行没有访问到)- 这些旧依赖需要被清理掉
5. clearTracking - 清理依赖
function clearTracking(link: Link) {
while (link) {
const { prevSub, nextSub, nextDep, dep } = link;
// 从 dep 的订阅者链表中移除
if (prevSub) {
prevSub.nextSub = nextSub;
} else {
dep.subs = nextSub;
}
if (nextSub) {
nextSub.prevSub = prevSub;
} else {
dep.subsTail = prevSub;
}
// 清空节点引用
link.dep = link.sub = undefined;
// 回收到对象池
link.nextDep = linkPool;
linkPool = link;
link = nextDep;
}
}
关键点:
- 双向链表的删除操作
- 将清理的节点回收到对象池
- 避免内存泄漏
6. propagate - 传播更新
function propagate(subs) {
let link = subs;
let queuedEffect = [];
while (link) {
const sub = link.sub;
// 只通知未被标记为 dirty 的 effect
if (!sub.tracking && !sub.dirty) {
sub.dirty = true;
if ("update" in sub) {
// 计算属性特殊处理
processComputedUpdate(sub);
} else {
// 普通 effect 加入队列
queuedEffect.push(sub);
}
}
link = link.nextSub;
}
// 批量执行所有 effect
queuedEffect.forEach((effect) => effect.notify());
}
优化策略:
- 去重:通过
dirty标记避免重复触发 - 批量执行:先收集需要执行的 effect,再统一执行
- 计算属性优先:有
update方法的(computed)优先处理
调度器机制
1. 什么是调度器?
调度器(Scheduler)是 Vue3 响应式系统的核心特性,它允许自定义 effect 的执行时机和方式。
2. 默认调度器
class ReactiveEffect {
notify() {
this.scheduler(); // 数据变化时调用
}
scheduler() {
this.run(); // 默认行为:立即执行
}
}
3. 自定义调度器
通过 options.scheduler 可以自定义执行逻辑:
const runner = effect(
() => {
console.log("effect run");
},
{
scheduler: () => {
console.log("自定义调度");
// 可以控制何时执行 effect
},
}
);
4. 调度器的应用场景
场景 1:异步更新
let queue = [];
let isFlushing = false;
effect(
() => {
console.log(state.count);
},
{
scheduler: () => {
if (!isFlushing) {
isFlushing = true;
Promise.resolve().then(() => {
queue.forEach((job) => job());
queue = [];
isFlushing = false;
});
}
queue.push(() => {
// 执行 effect
});
},
}
);
作用: 合并同步的多次更新,在微任务中统一执行
场景 2:watch 的实现
function watch(source, cb) {
let oldValue;
const effect = new ReactiveEffect(getter);
// 自定义调度器
effect.scheduler = () => {
const newValue = effect.run();
cb(newValue, oldValue); // 执行回调
oldValue = newValue;
};
oldValue = effect.run(); // 初始执行,收集依赖
}
工作流程:
- 第一次执行
effect.run()收集依赖,获取oldValue - 数据变化时触发
scheduler - 在
scheduler中获取newValue并调用用户回调 - 更新
oldValue为newValue
场景 3:条件执行
effect(
() => {
console.log("effect");
},
{
scheduler: () => {
if (shouldRun) {
// 只在满足条件时执行
effect.run();
}
},
}
);
5. 调度器的执行时机
数据变化
↓
触发 set
↓
trigger
↓
propagate
↓
effect.notify()
↓
effect.scheduler() ← 这里可以自定义
↓
effect.run()
6. Options 参数的处理
function effect(fn, options) {
const e = new ReactiveEffect(fn);
// 通过 Object.assign 将用户传入的 scheduler 覆盖默认的
// 实例属性优先级高于原型属性
Object.assign(e, options);
e.run();
const runner = e.run.bind(e);
runner.effect = e;
return runner;
}
关键点:
Object.assign会将options.scheduler覆盖原型上的scheduler方法- 实例属性的优先级高于原型属性
- 这是 JavaScript 属性查找机制的应用
核心概念总结
1. 响应式数据的本质
数据 (Dependency)
↕ 双向链表
Effect (Subscriber)
- Dependency:响应式数据(ref/reactive 的属性)
- Subscriber:副作用函数(effect)
- Link:连接两者的链表节点
2. 依赖收集的时机
effect(() => {
// 在 effect 执行期间
const value = ref.value; // ← 此时收集依赖
});
条件:
- 必须在 effect 执行期间
- 必须访问响应式数据的 getter
3. 派发更新的时机
ref.value = newValue; // ← set 时触发
流程:
- 调用 setter
- 判断值是否变化(
hasChanged) - 触发
trigger - 遍历订阅者链表
- 调用每个 effect 的
notify方法 - 执行
scheduler
4. 双向链表的优势
| 优势 | 说明 |
|---|---|
| 快速添加 | 通过 tail 指针 O(1) 添加节点 |
| 快速删除 | 双向指针支持 O(1) 删除 |
| 节点复用 | 对象池复用,减少 GC |
| 灵活遍历 | 支持双向遍历 |
| 自动清理 | 轻松识别和清理过期依赖 |
5. 调度器的作用
| 应用场景 | 作用 |
|---|---|
| 异步更新 | 合并多次更新,减少执行次数 |
| watch | 在数据变化时执行回调 |
| computed | 延迟计算,按需更新 |
| 自定义逻辑 | 完全控制 effect 执行时机 |
6. 重要的设计模式
对象池模式
let linkPool: Link; // 复用清理的节点
发布-订阅模式
// 订阅
link(dep, sub);
// 发布
propagate(dep.subs);
模板方法模式
notify() {
this.scheduler(); // 父类定义流程,子类实现细节
}
学习心得
今日重点
- ✅ Ref 的实现:通过 getter/setter 拦截访问,实现依赖收集和派发更新
- ✅ Effect 的基础:理解 run 方法的执行流程,嵌套 effect 的处理
- ✅ 双向链表:Vue3 使用双向链表管理依赖关系,支持高效的增删操作
- ✅ 调度器机制:通过 scheduler 实现灵活的 effect 执行策略
关键收获
- activeSub:当前正在执行的 effect,是依赖收集的核心
- startTrack/endTrack:精确控制依赖收集的时机和清理逻辑
- 节点复用:对象池模式减少 GC 压力
- dirty 标记:避免重复触发,提升性能
待深入学习
- Reactive 的实现(对象的响应式)
- Computed 的惰性计算和缓存机制
- Watch 的 deep、immediate、once 等选项
- 调度器的批量更新策略
- 异步组件的更新机制
代码示例
完整的 Effect 使用示例
import { ref, effect } from "@vue/reactivity";
// 1. 创建响应式数据
const count = ref(0);
const name = ref("Vue");
// 2. 创建 effect
const runner = effect(() => {
console.log(`${name.value}: ${count.value}`);
});
// 输出: Vue: 0
// 3. 修改数据,自动触发 effect
count.value++;
// 输出: Vue: 1
name.value = "React";
// 输出: React: 1
// 4. 停止 effect
runner.effect.stop();
count.value++;
// 不再输出(effect 已停止)
自定义调度器示例
import { ref, effect } from "@vue/reactivity";
const count = ref(0);
// 使用自定义调度器
effect(
() => {
console.log("Effect run:", count.value);
},
{
scheduler: () => {
console.log("Scheduled!");
// 可以在这里控制 effect 的执行
},
}
);
// 输出: Effect run: 0 (初次执行)
count.value++;
// 输出: Scheduled! (触发调度器,不是直接执行 effect)
学习总结: Vue3 的响应式系统通过精巧的双向链表设计,实现了高效的依赖追踪和更新派发。调度器机制赋予了开发者极大的灵活性,使得 Vue3 能够在保持简洁 API 的同时,支持复杂的响应式场景。
下一步计划: 深入学习 Reactive、Computed 的实现,以及 Vue3 的组件更新机制。