设计与实现
约 2007 字大约 7 分钟
2025-11-17
响应系统
响应系统是 Vue.js 的核心,是响应式数据的关键。
基本实现
一般的 JS 对象数据并不会追踪更新,当它被改变时,那些依赖它的副作用函数并不知道它变了。
const obj = {
name: "张三",
age: 18,
};
function changeAge() {
document.body.innerHTML = obj.age;
}
changeAge();
setTimeout(() => {
obj.age = 19; // 1 秒后 age 改变,但视图不会更新
}, 1000);上面的例子中,虽然 obj.age 被改变了,但函数 changeAge 并不知道它被改变了,因此它也不会被执行,视图就不会更新。所以,如果我们 能够拦截一个对象的读取和更改,就能够追踪到数据变化。
Proxy 是 ES6 新增的 API,它可以拦截对目标对象的操。例如对象被访问了、被更改了,并可以自定义这些行为。所以我们可以创建一个 Proxy 来拦截对对象属性的访问和修改,来实现响应式数据。
- 创建一个 Proxy 对象
创建一个基本对象,并使用 Proxy 进行代理,自定义 get 与 set 行为。
const obj = {
name: "张三",
age: 18,
};
const proxyData = new Proxy(obj, {
get(target, key) {
return target[key];
},
set(target, key, value) {
target[key] = value;
return true;
},
});- 添加副作用函数
当数据变化时,还需要在 set 中添加副作用函数。这样,当数据被修改时才会执行副作用函数。
我们可以使用 Set 来保存副作用函数,在 get 中使用 add 方法添加,在 set 中使用 forEach 方法遍历执行。
// 原始数据
const obj = {
name: "Tom",
age: 18,
};
// 副作用函数
function effect() {
document.body.innerText = proxyData.name;
}
// 使用 Set 保存副作用函数
const effectFn = new Set();
// 代理对象
const proxyData = new Proxy(obj, {
get(target, key) {
// 添加副作用函数
effectFn.add(effect);
return target[key];
},
set(target, key, value) {
target[key] = value;
// 遍历副作用函数并执行
effectFn.forEach((fn) => fn());
return true;
},
});
// 执行副作用函数,触发读取
effect();
// 1 秒后修改响应式数据
setTimeout(() => {
proxyData.name = "Bob";
}, 1000);在上面的代码中,使用 Set 保存副作用函数,实现当更改代理对象时,视图也会更新。(若你没有看到界面更新,请刷新页面)
在 Vue3 中,便是使用 Proxy 实现响应式数据,是响应系统的核心。上面的例子实现了一个最基本的响应系统,但完整的响应系统还有更多细节, 比如还需要注册副作用函数(上面的例子讲副作用函数写死了)、依赖收集等。
完善响应系统
上面的例子中,我们直接将副作用函数 effect 在 get 中添加,这样显然是不行的。因为,副作用函数不一定叫 effect,而且它还可能是一个匿名函数或者箭头函数。 另外一个对象可能有很多个字段,而每个字段又可能有很多个副作用函数,当我们需要更改某一个字段的数据时,Proxy 中根本不知道该字段对应的哪些副作用函数需要执行, 我们也不可能将所有副作用函数都添加到 Proxy 中。因此我们需要提供一个用来注册副作用函数的机制,并且还要改变字段与副作用函数关系的数据结构。
注册副作用函数
在 Vue3 中,定义了一个 effect 函数,用来注册副作用函数。它接受一个参数,即副作用函数,并将它赋值给一个全局变量 activeEffect,存储当前活跃的副作用函数。同时执行副作用函数。
// 用一个全局变量存储被注册的副作用函数
let activeEffect;
// effect 函数用于注册副作用函数
function effect(fn) {
// 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}需要再次执行副作用函数时,只需要调用 effect 函数即可。
effect(() => {
proxyData.name = "Bob";
});依赖收集
依赖收集是实现响应系统较难的一环节。在上面的例子中,我们没有在副作用函数与被操作的目标字段之间建立任何联系,这就会导致我们更改了某一个字段,其它字段的副作用函数可能会被执行。 如果我们将 key 与 effect 关联起来,那么当 key 被修改时,与它关联的 effect 函数就会执行,依赖就会被正确收集。
一个目标对象 target 中有多个字段 key,每个字段都有多个副作用函数 effect,那么 key 与 effect 之间的关系可以表示为 Map。 而 target 与 key 之间的关系可以使用 WeakMap 来表示。此时响应式对象 target 、属性 key 与副作用函数 effect 之间的关系可以表示为 WeakMap<target, Map<key, Set<effect>>>:
targetMap: WeakMap<
target, // 响应式对象(target)
Map< // depsMap
key, // 属性名(key)
Set<ReactiveEffect> // 依赖该属性的副作用函数
>
>为什么target 与 key 使用 WeakMap ?
WeakMap 的键值对是弱引用,且键必须是对象。使用 WeakMap 是为了让响应式对象在外部无引用时能被自动垃圾回收,避免内存泄漏。
使用这种结构来关联原对象与副作用函数关系,则每一个 key 都能够与依赖自己属性的副作用函数进行关联。响应系统的实现流程则大体如下:
- 创建一个注册副作用函数的函数
effect,并将副作用函数赋值给activeEffect。 - 创建一个响应式对象
target,并使用Proxy进行代理,定义get和set拦截器。 - 在
get中,判断当前副作用函数是否是在key下,否则添加。 - 在
set中,触发target的depsMap中所有副作用函数的执行。
// 原始数据
const data = { text: "hello world", to: "world" };
// 存储副作用函数
const bucket = new WeakMap();
// 当前激活的副作用函数
let activeEffect = null;
// effect 函数用于注册副作用函数
function effect(fn) {
// 将副作用函数包装一层
const effectFn = () => {
// 执行副作用函数前,将其设置为当前激活的副作用函数
activeEffect = effectFn;
fn();
};
// 立即执行一次副作用函数
effectFn();
}
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 没有 activeEffect,直接 return
if (!activeEffect) return target[key];
// 根据 target 从“桶”中取得 depsMap,它也是一个 Map 类型:key --> effects
let depsMap = bucket.get(target);
// 如果不存在 depsMap,那么新建一个 Map 并与 target 关联
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
// 再根据 key 从 depsMap 中取得 deps,它是一个 Set 类型,
// 里面存储着所有与当前 key 相关联的副作用函数:effects
let deps = depsMap.get(key);
// 如果 deps 不存在,同样新建一个 Set 并与 key 关联
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
// 最后将当前激活的副作用函数添加到“桶”里
deps.add(activeEffect);
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal) {
// 设置属性值
target[key] = newVal;
// 根据 target 从桶中取得 depsMap,它是 key --> effects
const depsMap = bucket.get(target);
if (!depsMap) return;
// 根据 key 取得所有副作用函数 effects
const effects = depsMap.get(key);
// 执行副作用函数
effects && effects.forEach((fn) => fn());
},
});上面的例子中,我们实现的 Proxy 拦截都是浅层的,而在 Vue3 中,如 reactive() 函数会进行深度监听。其核心同样是使用 Proxy 拦截,但需要使用递归监听嵌套对象。 下面是一个示例代码:
// Vue 3 类似的实现思路
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key); // 依赖收集
const res = target[key];
if (typeof res === 'object' && res !== null) {
return reactive(res); // 递归代理
}
return res;
},
set(target, key, value) {
trigger(target, key); // 触发更新
target[key] = value;
return true;
}
});
}