导语:
你是否好奇Vue/React的响应式原理?想自己实现一个“自动更新”的魔法系统?本文通过最简代码还原响应式核心逻辑,并揭示4个初学者必踩的坑。读完你将掌握:依赖收集、派发更新、嵌套effect等核心概念,写出更健壮的响应式代码!
一、响应式系统的“骨架”
想象一个智能Excel表格:当某个单元格数据变化时,依赖它的图表会自动刷新。响应式系统的核心就是:
- 依赖收集(谁用了我?)
- 派发更新(我变了,通知谁?)
我们用WeakMap存储依赖关系,Proxy拦截对象操作,先看基础实现👇:
const targetMap = new WeakMap()
let activeEffect = null
// 核心三件套
export const effect = (fn) => { /*...*/ } // 副作用函数
export const reactive = (target) => { /*...*/ } // 响应式对象
const track = () => { /*...*/ } // 收集依赖
const trigger = () => { /*...*/ } // 触发更新
二、简化版的Vue 3响应式系统
const targetMap = new WeakMap() // 存储依赖关系
let activeEffect = null // 当前执行的副作用函数
// 副作用函数
export const effect = fn => {
activeEffect = fn
fn()
activeEffect = null
}
// 收集依赖
const track = (target, prop) => {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) targetMap.set(target, (depsMap = new Map()))
let dep = depsMap.get(prop)
if (!dep) depsMap.set(prop, (dep = new Set()))
dep.add(activeEffect)
}
// 触发更新
const trigger = (target, prop) => {
const depsMap = targetMap.get(target)
if (!depsMap) return
const effects = depsMap.get(prop)
if (effects) effects.forEach(effect => effect())
}
// 响应式对象
export const reactive = target => {
return new Proxy(target, {
get(target, prop, receiver) {
track(target, prop)
return Reflect.get(target, prop, receiver)
},
set(target, prop, value, receiver) {
const result = Reflect.set(target, prop, value, receiver)
trigger(target, prop)
return result
},
})
}
三、新手必踩的4个坑(附解决方案)
陷阱1:嵌套Effect导致依赖丢失
场景:组件嵌套时,内部组件渲染会覆盖外部effect
现象:外层数据变化不再触发更新
解法:用“函数调用栈”管理effect层级
const effectStack = [] // 新增栈结构
const execute = () => {
effectStack.push(execute) // 入栈
activeEffect = execute
fn()
effectStack.pop() // 出栈
activeEffect = effectStack[effectStack.length - 1] // 恢复上一层
}
陷阱2:相同值重复触发更新
场景:obj.count = 1多次赋值相同值
现象:引发不必要渲染甚至死循环
解法:新旧值对比后再触发
// set拦截器中添加判断
if (!hadKey || !Object.is(value, oldValue)) {
trigger(target, prop)
}
陷阱3:数组长度变化无响应
场景:arr[10] = 1导致length变化
现象:监听length的属性不更新
解法:新增元素时主动触发length更新
if (Array.isArray(target) && !hadKey) {
trigger(target, 'length') // 对数组特殊处理
}
陷阱4:删除属性无响应
场景:delete obj.prop
现象:依赖该属性的视图不更新
解法:拦截delete操作
deleteProperty(target, prop) {
if (hadKey) trigger(target, prop) // 触发更新
return result
}
四、问题总结及改进
问题总结:
- 嵌套 Effect 问题:当 Effect 嵌套时,内部的 Effect 执行后会将
activeEffect置为null,导致外层 Effect 后续的依赖收集失败。 - 相同值触发更新:设置属性时未检查新旧值是否相同,导致不必要的更新和可能的无限循环。
- 数组长度变化未触发更新:通过索引设置数组元素时,若隐式改变
length属性,未触发相关依赖。 - 删除属性未触发更新:未处理
delete操作,导致属性删除时依赖不更新。
改进后的代码:
const targetMap = new WeakMap(); // 存储依赖关系
let activeEffect = null; // 当前执行的副作用函数
const effectStack = []; // 处理嵌套 Effect
// 副作用函数
export const effect = (fn) => {
const execute = () => {
try {
effectStack.push(execute);
activeEffect = execute;
fn();
} finally {
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1] || null;
}
};
execute();
};
// 收集依赖
const track = (target, prop) => {
if (!activeEffect) return;
let depsMap = targetMap.get(target);
if (!depsMap) targetMap.set(target, (depsMap = new Map()));
let dep = depsMap.get(prop);
if (!dep) depsMap.set(prop, (dep = new Set()));
dep.add(activeEffect);
};
// 触发更新
const trigger = (target, prop) => {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const effects = depsMap.get(prop);
if (effects) {
// 避免在遍历过程中执行 effect 导致的清理和重新收集问题
new Set(effects).forEach(effect => effect());
}
};
// 响应式对象
export const reactive = (target) => {
return new Proxy(target, {
get(target, prop, receiver) {
track(target, prop);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
const oldValue = Reflect.get(target, prop, receiver);
const hadKey = Object.prototype.hasOwnProperty.call(target, prop);
const result = Reflect.set(target, prop, value, receiver);
// 触发更新条件:新增属性或值发生变化
if (!hadKey) {
trigger(target, prop);
// 处理数组新增元素导致 length 变化
if (Array.isArray(target)) {
trigger(target, 'length');
}
} else if (!Object.is(value, oldValue)) {
trigger(target, prop);
}
return result;
},
deleteProperty(target, prop) {
const hadKey = Object.prototype.hasOwnProperty.call(target, prop);
const result = Reflect.deleteProperty(target, prop);
if (hadKey) {
trigger(target, prop);
}
return result;
}
});
};
改进说明:
- 嵌套 Effect:使用
effectStack栈结构管理嵌套 Effect,确保内外层 Effect 正确追踪。 - 值变化检查:使用
Object.is比较新旧值,避免相同值触发更新。 - 数组长度处理:在新增数组元素时(属性不存在的情况),主动触发
length属性的更新。 - 删除属性:拦截
deleteProperty操作并触发更新,确保依赖跟踪的正确性。
注意事项:
- 数组方法处理:通过
push、pop等方法修改数组时,需确保所有相关属性(如索引和length)触发更新。此实现已部分处理,但更复杂的数组操作可能需要额外处理。 - 性能优化:
trigger中使用new Set(effects)避免在遍历过程中修改 Set 导致的无限循环问题。 - 边缘情况:如
NaN处理、符号属性等,根据需求可进一步扩展。
结语:
响应式系统如同精密的齿轮组,每一个边界case都可能导致“卡壳”。理解这些陷阱后,你不仅能更好地使用Vue/React,更能写出高可靠的底层库。动手实现一次,胜过阅读十篇原理分析
面试过程中一般时间紧,只需要写出简化版的响应式系统原理即可,但里面的坑也要必知必会,才能吊打面试官~~~