Vue数据响应式底层原理
数据变了,视图自动更新——这背后是怎么做到的?
什么是数据响应式
当数据发生变化时,自动执行依赖该数据的相关函数(如渲染函数)。
const data = { count: 0 }
autorun(() => {
document.body.innerHTML = data.count // 用到了 count
})
data.count = 1 // → 自动触发上面的函数,页面更新
Vue 2:Object.defineProperty
核心思路:劫持属性的读写,读取时收集依赖,写入时派发更新。
function observe(obj) {
for (const key in obj) {
let internalValue = obj[key]
let funcs = [] // 依赖该属性的所有函数
Object.defineProperty(obj, key, {
get() {
// 依赖收集:记录谁在用我
if (window.__func && !funcs.includes(window.__func)) {
funcs.push(window.__func)
}
return internalValue
},
set(val) {
internalValue = val
// 派发更新:通知依赖我的函数重新执行
funcs.forEach(fn => fn())
}
})
}
}
function autorun(fn) {
window.__func = fn // 标记当前正在运行的函数
fn() // 执行(触发 get,完成依赖收集)
window.__func = null
}
执行流程
第一步:依赖收集(读数据时)
autorun(fn)
→ window.__func = fn
→ fn() 执行,读取 data.count
→ 触发 count 的 get
→ fn 被记录进 funcs
→ window.__func = null
第二步:派发更新(写数据时)
data.count = 1
→ 触发 count 的 set
→ 遍历 funcs,重新执行所有函数
→ 视图更新
Object.defineProperty 的三个天生缺陷
这不是代码写得不好,是 Object.defineProperty 这个 API 本身的限制。
缺陷一:无法检测新增属性
observe(data)
data.newKey = 'hello' // ❌ 新增的属性没有 getter/setter,完全不响应
根本原因:observe 只能对已存在的属性做劫持,对后来新增的属性无能为力。
这就是 Vue 2 中需要 Vue.set(obj, key, value) 的原因——它会手动触发响应。
缺陷二:无法检测数组下标变更
observe(data)
data.list[0] = 'new' // ❌ 无法触发更新
data.list.length = 0 // ❌ 无法触发更新
Vue 2 的解决方案:重写数组的 7 个变异方法(push、pop、shift、unshift、splice、sort、reverse),让它们在调用时手动触发更新。
所以 Vue 2 中这样写才有响应:
data.list.push('new') // ✅ 走的是重写后的 push,会触发更新
data.list.splice(0, 1) // ✅ 同上
缺陷三:嵌套对象需要递归处理
const data = { user: { name: 'Alice' } }
observe(data)
data.user.name = 'Bob' // ❌ user 内部没有被 observe,不响应
Vue 2 的解决方案:初始化时递归遍历所有嵌套对象,全部做 defineProperty。
带来的性能问题:数据层级越深,初始化越慢——无论你用不用深层数据,都会全部遍历一遍。
Vue 3:Proxy 全面升级
Vue 3 用 Proxy 替换 Object.defineProperty,从根本上解决了上面三个问题。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key) // 依赖收集
const val = target[key]
return typeof val === 'object' ? reactive(val) : val // 惰性递归
},
set(target, key, value) {
target[key] = value
trigger(target, key) // 派发更新
return true
}
})
}
为什么 Proxy 能解决这些问题?
- 新增属性:Proxy 代理的是整个对象,任何属性的读写都会被拦截,包括新增的
- 数组下标:
arr[0] = 'new'也是属性写入,Proxy 的set直接捕获 - 嵌套对象:不需要初始化时全量递归,访问到某个属性时再代理(惰性),性能更好
Vue 2 vs Vue 3 对比
| Vue 2(defineProperty) | Vue 3(Proxy) | |
|---|---|---|
| 新增属性 | ❌ 不响应,需 Vue.set | ✅ 自动响应 |
| 数组下标 | ❌ 不响应,需重写方法 | ✅ 自动响应 |
| 嵌套对象 | ⚠️ 初始化全量递归,性能差 | ✅ 访问时按需代理,性能好 |
| 浏览器兼容 | ✅ IE9+ | ❌ 不支持 IE |
一句话总结
Vue 响应式的本质是拦截数据的读(收集依赖)和写(派发更新)。Vue 2 用
Object.defineProperty实现,存在新增属性、数组下标、性能三个天生缺陷;Vue 3 改用Proxy,一次性全部解决。

浙公网安备 33010602011771号