JavaScript响应式系统的核心目标是当数据发生变化时,自动通知所有依赖该数据的地方进行更新,而依赖收集和触发更新就是实现这一目标的两个核心步骤。依赖收集负责在数据被访问时记录下依赖该数据的副作用函数,触发更新则负责在数据被修改时执行这些副作用函数。

依赖收集的实现原理
要实现依赖收集,首先需要拦截数据的读取操作,在读取数据时把当前的副作用函数存起来。早期很多框架使用Object.defineProperty实现数据拦截,现在更推荐用Proxy,因为它可以拦截更多操作,也不需要遍历对象的所有属性。
我们可以设计一个全局变量来存储当前正在执行的副作用函数,当数据被读取时,把这个副作用函数收集到一个依赖集合中。
// 存储当前正在执行的副作用函数
let activeEffect = null;
// 依赖收集桶,结构为 WeakMap<target, Map<key, Set<effect>>>
const bucket = new WeakMap();
// 依赖收集函数
function track(target, key) {
// 如果没有正在执行的副作用函数,直接返回
if (!activeEffect) return;
// 获取target对应的依赖map
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
// 获取key对应的副作用函数集合
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
// 把当前副作用函数加入集合
deps.add(activeEffect);
}
使用Proxy拦截数据读取
通过Proxy代理对象,在get拦截器中调用track函数完成依赖收集:
// 创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
// 收集依赖
track(target, key);
// 返回属性值
return Reflect.get(target, key, receiver);
},
set() {
// 暂时先不实现,后面讲触发更新时补充
}
});
}
副作用函数的注册
我们需要一个函数来注册副作用函数,执行传入的函数时会把该函数赋值给activeEffect,方便依赖收集时使用:
function effect(fn) {
// 把fn赋值给activeEffect
const effectFn = () => {
activeEffect = effectFn;
// 执行fn,触发get拦截器,完成依赖收集
fn();
activeEffect = null;
};
// 立即执行一次
effectFn();
}
触发更新的实现原理
触发更新的核心是拦截数据的修改操作,当数据被修改时,从依赖收集桶中找到对应的副作用函数集合,依次执行这些函数。
完善Proxy的set拦截器
在set拦截器中调用触发更新的逻辑:
function trigger(target, key) {
// 获取target对应的依赖map
const depsMap = bucket.get(target);
if (!depsMap) return;
// 获取key对应的副作用函数集合
const effects = depsMap.get(key);
if (!effects) return;
// 执行所有副作用函数
effects.forEach(effectFn => effectFn());
}
// 完善reactive的set拦截器
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
// 设置属性值
const result = Reflect.set(target, key, newValue, receiver);
// 触发更新
trigger(target, key);
return result;
}
});
}
完整示例与测试
下面是一个完整的简易响应式系统示例,我们可以测试数据变化后副作用函数是否自动执行:
// 之前定义的变量和函数
let activeEffect = null;
const bucket = new WeakMap();
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
depsMap = new Map();
bucket.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(activeEffect);
}
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
if (!effects) return;
effects.forEach(effectFn => effectFn());
}
function effect(fn) {
const effectFn = () => {
activeEffect = effectFn;
fn();
activeEffect = null;
};
effectFn();
}
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, newValue, receiver) {
const result = Reflect.set(target, key, newValue, receiver);
trigger(target, key);
return result;
}
});
}
// 测试代码
const obj = reactive({ count: 0 });
effect(() => {
console.log('count的值是:', obj.count);
});
// 首次执行会输出 count的值是: 0
obj.count = 1;
// 数据修改后触发更新,输出 count的值是: 1
注意事项与优化方向
- 上面的实现中,如果副作用函数执行时抛出错误,需要把
activeEffect重置为null,避免影响后续依赖收集,可以在effectFn中加入try...finally处理。 - 当对象新增属性或者删除属性时,上面的逻辑也能正常触发更新,因为
Proxy可以拦截这些操作,而Object.defineProperty无法拦截新增属性,这也是现在更推荐用Proxy的原因。 - 实际框架中的响应式系统会更复杂,比如会处理嵌套对象、数组方法拦截、调度执行副作用函数等场景,但核心的依赖收集和触发更新逻辑和上面的示例是一致的。
JavaScript响应式系统依赖收集触发更新Proxy修改时间:2026-06-15 20:45:26