1. 从零手写 Vue 响应式系统:揭秘 watch 与 deep 的底层逻辑
"为什么我的 watch 监听对象没反应?"、"面试官问响应式原理该怎么答?"、"deep: true 到底监控了什么?"——这些问题困扰过无数 Vue 开发者。今天我们不读源码不背八股,直接动手用几十行代码实现一个迷你响应式系统,彻底搞懂这些机制。
2. Proxy 门禁系统:响应式的基石
Vue 3 的响应式核心基于 ES6 的 Proxy。我们可以把 Proxy 想象成一个智能门禁系统:
javascript复制const targetObj = { a: 1 };
const proxyObj = new Proxy(targetObj, {
get(target, key) {
console.log(`有人读取了${key}`);
return target[key];
},
set(target, key, value) {
console.log(`有人修改了${key}为${value}`);
target[key] = value;
return true;
}
});
这个基础版本有两个关键点:
- get 必须返回 target[key],否则外部读取永远得到 undefined
- set 必须返回 true,否则严格模式下会报错
实际项目中,Proxy 的 handler 有 13 种拦截操作,但 Vue 主要用 get/set/deleteProperty/has/ownKeys 这五种
3. 依赖收集与派发更新:响应式的灵魂
单纯的拦截还不够,我们需要实现 Vue 的核心机制:
3.1 核心角色定义
javascript复制// 当前正在执行的副作用函数
let activeEffect = null
// 依赖收集器(使用 WeakMap 避免内存泄漏)
const targetMap = new WeakMap()
// 注册副作用函数
function effect(fn) {
activeEffect = fn
fn() // 执行时会触发 getter 收集依赖
activeEffect = null
}
3.2 增强版 Proxy 实现
javascript复制function reactive(target) {
return new Proxy(target, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
return true
}
})
}
// 依赖收集
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)
}
// 派发更新
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
4. watch 的实现原理
4.1 基础版 watch
javascript复制function watch(source, cb) {
effect(() => {
// 这里读取 source 触发依赖收集
if (typeof source === 'function') {
source()
} else {
traverse(source)
}
cb()
})
}
4.2 递归遍历函数
javascript复制function traverse(value, seen = new Set()) {
if (typeof value !== 'object' || value === null || seen.has(value)) {
return
}
seen.add(value)
for (const key in value) {
traverse(value[key], seen)
}
return value
}
5. deep 参数的深层解析
5.1 不加 deep 的情况
javascript复制const obj = reactive({ a: { b: 1 } })
watch(() => obj.a, (val) => {
console.log('obj.a changed:', val)
})
obj.a.b = 2 // 不会触发
obj.a = { b: 3 } // 会触发
原因:
- 只监听了 obj.a 这个属性的引用变化
- 修改 a 的内部属性不会触发 a 的 setter
5.2 加 deep 的情况
javascript复制watch(() => obj.a, (val) => {
console.log('obj.a changed:', val)
}, { deep: true })
obj.a.b = 2 // 会触发
obj.a = { b: 3 } // 会触发
实现原理:
- 通过 traverse 递归访问所有属性
- 每个属性的 getter 都被触发
- 所有层级的属性都收集了当前 watcher 作为依赖
6. 性能优化与最佳实践
6.1 避免不必要的 deep
javascript复制// 不推荐
watch(obj, () => {...}, { deep: true })
// 推荐:明确指定要监听的属性
watch(() => obj.importantProp, () => {...})
6.2 使用 computed 优化
javascript复制const importantValue = computed(() => obj.a.b + obj.c.d)
watch(importantValue, (val) => {
console.log('重要值变化:', val)
})
6.3 复杂对象的处理策略
对于大型对象可以考虑:
- 分割成多个响应式对象
- 使用 shallowRef 配合手动 trigger
- 使用自定义的 diff 算法
7. 常见问题排查指南
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| watch 不触发 | 1. 监听的不是响应式对象 2. 修改的是嵌套属性但没加 deep |
1. 确保使用 reactive/ref 2. 添加 deep 或改为监听具体属性 |
| 无限循环 | 回调中修改了监听的值 | 1. 添加条件判断 2. 使用 nextTick |
| 性能问题 | 1. 监听了大对象 2. deep 监听复杂结构 |
1. 优化数据结构 2. 使用更精确的监听 |
8. 手写实现总结
通过这个迷你实现,我们理解了:
- Proxy 如何拦截操作
- 依赖收集的时机和方式
- effect 如何与响应式系统交互
- watch 和 deep 的真实工作原理
在实际项目中,Vue 的实现会更加复杂,考虑了更多边界情况和性能优化,但核心原理是一致的。理解这些基础概念后,再阅读 Vue 源码会轻松很多。
9. 面试回答指南
当被问到"为什么 watch 对象要加 deep"时,可以这样回答:
"Vue 的响应式系统默认只追踪直接访问的属性。当我们 watch 一个对象时,如果不加 deep,Vue 只会监听这个对象引用的变化。加上 deep 后,Vue 会递归遍历对象的所有属性,为每个属性都建立依赖关系,这样嵌套属性的变化也能被检测到。但要注意 deep 会带来性能开销,应该谨慎使用。"
这个回答展示了:
- 对响应式系统的理解深度
- 知道默认行为和 deep 的区别
- 有性能优化的意识
10. 扩展思考
如果想进一步深入,可以考虑:
- 如何实现 watch 的 immediate 选项?
- 怎样优化 deep 遍历的性能?
- Vue 2 的 defineProperty 实现有哪些不同?
- 如何实现类似 React 的 useMemo 功能?
这些问题的探索会让你对响应式系统有更全面的理解。