一、简单版本的响应式系统
javascript
// 存储依赖的全局桶
const bucket = new WeakMap();
// 当前激活的副作用函数
let activeEffect;
// 响应式对象
const data = { text: 'hello' };
const obj = new Proxy(data, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
return true;
}
});
// 依赖收集
function track(target, key) {
if (!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap) {
bucket.set(target, (depsMap = new Map()));
}
let deps = depsMap.get(key);
if (!deps) {
depsMap.set(key, (deps = new Set()));
}
deps.add(activeEffect);
}
// 依赖触发
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn());
}
// 副作用函数注册逻辑(示例)
function effect(fn) {
activeEffect = fn;
fn(); // 首次执行以触发依赖收集
}
验证
javascript
// 示例:验证响应式更新
effect(() => {
console.log('Effect triggered:', obj.text);
});
obj.text = 'vue3'; // 输出 "Effect triggered: vue3"
存在的问题
js
const data = {ok:true, text: 'hello world'}
const obj = new Proxy(data,{/**/})
effect(function effectFn() {
document.body.innerText = obj.ok ? obj.text : 'not'
})
当 effectFn 执行时会触发 obj.ok 和 obj.text 的读取操作,进而该副作用函数会被这两个字段收集进依赖集合。
当修改 obj.ok 为 false,并触发副作用函数重新执行后,document.body.innerText 必为 'not',无论 obj.text 如何修改。
但是事实并非如此,当我们修改 obj.text 时,副作用函数会被重新执行。
以上说明副作用函数中的分支切换会产生遗留的副作用函数,遗留的副作用函数会导致不必要的更新。
解决这个问题就是要在每次副作用函数执行时,先把该副作用函数从所有与之有关的依赖集合里删除,当副作用函数执行完毕后,会重新建立联系。
于是我们可以设计 effectFn.deps 属性来存储包含当前副作用函数的依赖集合。
二、优化后的响应式系统
js
// 存储依赖关系的容器(WeakMap<target, Map<key, Set<effect>>>)
const targetMap = new WeakMap()
// 当前激活的副作用函数
let activeEffect = null
// 副作用函数包装器
function effect(fn) {
const effectFn = () => {
cleanup(effectFn) // 执行清理
activeEffect = effectFn
fn()
}
effectFn.deps = [] // 存储关联的依赖集合
effectFn()
}
// 清理函数
function cleanup(effectFn) {
for (const dep of effectFn.deps) {
dep.delete(effectFn)
}
effectFn.deps.length = 0
}
// 依赖收集
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
depsMap = new Map()
targetMap.set(target, depsMap)
}
let dep = depsMap.get(key)
if (!dep) {
dep = new Set()
depsMap.set(key, dep)
}
dep.add(activeEffect)
activeEffect.deps.push(dep) // 反向记录依赖集合
}
// 触发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(key)
if (effects) {
// 创建副本防止无限循环
const effectsToRun = new Set(effects)
effectsToRun.forEach(effect => effect())
}
}
// 创建响应式对象
function reactive(data) {
return new Proxy(data, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
三、关键流程总结
首次执行流程

分支切换后重新执行流程

依赖关系变化对比
阶段 | obj.ok 的依赖集合 |
obj.text 的依赖集合 |
---|---|---|
首次执行后 | [effectFn] | [effectFn] |
修改 obj.ok=false 后 |
[effectFn](重新添加) | [](已清理) |
四、关键优化点说明
-
双向依赖记录:
- 每个属性维护自己的依赖集合(
Set<effect>
) - 每个effect维护自己关联的依赖集合(
deps: Set[]
)
- 每个属性维护自己的依赖集合(
-
cleanup机制:
scssfunction cleanup(effectFn) { // 遍历所有关联的依赖集合 for (const dep of effectFn.deps) { // 从依赖集合中移除当前effect dep.delete(effectFn) } // 清空关联记录 effectFn.deps.length = 0 }
-
执行时序控制:
- 在每次effect执行前先清理旧依赖
- 执行时重新建立新依赖
- 通过
activeEffect
标记当前激活的effect
五、总结
- 首次执行时机 :
effect
在定义时立即执行,确保首次渲染和依赖收集。 - cleanup 机制:每次重新执行副作用函数前,清理所有旧依赖,避免冗余更新。
- 动态依赖更新:重新执行时,根据当前条件分支访问的属性,动态建立新的依赖关系。
通过这一机制,响应式系统能够精确追踪依赖,避免不必要的计算和更新,从而显著提升性能。