手写响应式系统

根据响应式组件通知效果可以知道,响应式机制的主要功能就是,可以把普通的 JavaScript 对象封装成为响应式对象,拦截数据的获取和修改操作,实现依赖数据的自动化更新。

一个最简单的响应式模型,我们可以通过 reactive 函数,把数据包裹成响应式对象,并且通过 effect 函数注册回调函数,然后在数据修改之后,响应式地通知 effect 去执行回调函数即可。

整个流程这么概括地说,你估计不太理解,我们先通过一个简单的小例子直观感受一下响应式的效果。Vue 的响应式是可以独立在其他平台使用的。比如你可以新建 test.js,使用下面的代码在 node 环境中使用 Vue 响应。以 reactive 为例,我们使用 reactive 包裹 JavaScript 对象之后,每一次对响应式对象 counter 的修改,都会执行 effect 内部注册的函数:

1
2
3
4
5
6
7
8
9
10
11
12
import { effect, reactive } from "vue";

let dummy
const counter = reactive({ num1: 1, num2: 2 })
effect(() => {
dummy = counter.num1 + counter.num2
console.log(dummy)// 每次counter.num1修改都会打印日志
})
setInterval(()=>{
counter.num1++
},1000)

执行 node test.js 之后,你就可以看到 effect 内部的函数会一直调用,每次 count.value 修改之后都会执行。

接下来给出模块化实现,便于拆分理解与复用。

模块化实现(reactivity/)

为便于理解与复用,可将核心拆分为以下文件:

1
2
3
4
5
6
reactivity/
├── effect.js // ReactiveEffect / effect / scheduler
├── track.js // track / trigger / targetMap
├── reactive.js // reactive / Proxy
├── handlers.js // mutableHandlers
└── index.js // 使用示例

reactivity/effect.js

内容与功能说明:

  • 核心类 ReactiveEffect:封装副作用函数 fn 与可选 scheduler;支持 run() 执行与 stop() 解绑
  • 依赖反向记录:deps 保存该 effect 被哪些依赖集合收集,stop 时统一清理
  • effect(fn, { scheduler }):创建并立即执行,返回 runner 以便手动触发或复用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// effect.js
export let activeEffect = null;
export const effectStack = [];

export class ReactiveEffect {
constructor(fn, scheduler) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = []; // 该 effect 被哪些依赖集合收集
}
run() {
if (!this.active) return this.fn();
try {
effectStack.push(this);
activeEffect = this;
return this.fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
}
}
stop() {
if (this.active) {
// 解除所有依赖中的当前 effect
this.deps.forEach(dep => dep.delete(this));
this.deps.length = 0;
this.active = false;
}
}
}

export function effect(fn, options = {}) {
const _effect = new ReactiveEffect(fn, options.scheduler);
_effect.run();
// 返回 runner,以便手动触发
return _effect.run.bind(_effect);
}

reactivity/track.js

内容与功能说明:

  • 依赖桶结构:WeakMap<Target, Map<Key, Set<Effect>>>
  • track(target,key):在读取时把当前活跃 effect 收集到集合中,避免重复收集
  • trigger(target,key):在写入时取出集合执行;如存在 scheduler,则走调度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// track.js
import { activeEffect } from './effect.js';

export const targetMap = new WeakMap(); // target -> (key -> dep(Set))

export function track(target, key) {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(key);
if (!dep) depsMap.set(key, (dep = new Set()));
if (!dep.has(activeEffect)) {
dep.add(activeEffect);
// 反向记录,便于 stop 时清理
activeEffect.deps && activeEffect.deps.push(dep);
}
}

export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const dep = depsMap.get(key);
if (!dep) return;

// 拷贝,避免遍历过程中集合被改动
[...dep].forEach(effect => {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
});
}

reactivity/handlers.js

内容与功能说明:

  • get:通过 Reflect.get 取值并调用 track 收集依赖;对子对象做懒代理
  • set:仅当新旧值不等时调用 trigger 触发;保持与 Proxy 语义一致
  • reactive 解耦:职责仅为“拦截 + 依赖编织”,不关心缓存细节
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// handlers.js
import { track, trigger } from './track.js';
import { reactive } from './reactive.js';

const isObject = (v) => v !== null && typeof v === 'object';

export const mutableHandlers = {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
track(target, key);
return isObject(res) ? reactive(res) : res;
},
set(target, key, value, receiver) {
const old = target[key];
const ok = Reflect.set(target, key, value, receiver);
if (old !== value) trigger(target, key);
return ok;
}
};

reactivity/reactive.js

内容与功能说明:

  • 仅对象可代理:原始值直接返回
  • 代理缓存:同一对象多次 reactive 返回同一个代理,保证引用稳定
  • 职责单一:创建并返回代理,拦截逻辑由 handlers 提供
1
2
3
4
5
6
7
8
9
10
11
12
13
// reactive.js
import { mutableHandlers } from './handlers.js';

const isObject = (v) => v !== null && typeof v === 'object';
const reactiveCache = new WeakMap();

export function reactive(target) {
if (!isObject(target)) return target;
if (reactiveCache.has(target)) return reactiveCache.get(target);
const proxy = new Proxy(target, mutableHandlers);
reactiveCache.set(target, proxy);
return proxy;
}

reactivity/index.js(使用示例)

内容与功能说明:

  • 作为最简用法展示:reactive + effect 的数据联动
  • 可替换为任意 UI 层(控制台/DOM/框架),验证响应式最小闭环
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.js
import { reactive } from './reactive.js';
import { effect } from './effect.js';

const state = reactive({ count: 0 });

effect(() => {
console.log('count =>', state.count);
});

setInterval(() => {
state.count++;
}, 1000);

小结

以上代码展示了响应式系统的最小闭环:

  • 读取时 track 建立「属性 → 副作用」的依赖图;
  • 写入时 trigger 找到并执行相关副作用;
  • reactive 负责把对象读写转接到 track/trigger

在此基础上,再逐步扩展 ref / computed / watch / scheduler / cleanup 等,后面有时间在增加。