前言
Vue3 发布之后,响应式系统经历了一次重要升级。
Vue2 使用的是:
Object.defineProperty
Vue3 则全面改用了:
Proxy
很多文章都会告诉你:
Proxy 比 defineProperty 更强。
但很少解释:
为什么 Vue 官方要花这么大的代价重构整个响应式系统?
Proxy 到底解决了哪些历史遗留问题?
它又带来了哪些新的设计思路?
本文将从 JavaScript 底层原理出发,分析 Vue3 响应式系统背后的设计思想。
什么是响应式?
所谓响应式,其实就是:
数据变化时,自动通知依赖它的代码重新执行。
例如:
const state = {
count: 0
}
页面:
{{ state.count }}
当:
state.count++
页面自动更新。
这个过程:
开发者没有主动刷新页面。
而是框架帮我们完成了。
Vue2 为什么使用 defineProperty?
在 ES6 出现之前。
JavaScript 并没有 Proxy。
Vue2 只能:
拦截:
get
set
例如:
Object.defineProperty(obj,"count",{
get(){},
set(){}
})
每次读取:
进入 get。
每次修改:
进入 set。
Vue:
就是利用这个机制。
实现依赖收集。
defineProperty 的局限
随着项目越来越复杂。
问题开始暴露。
例如:
新增属性。
user.age=18
Vue2:
无法监听。
删除属性:
delete user.age
同样无法监听。
数组:
arr[5]="Vue"
也存在各种边界问题。
因此:
Vue2 才提供了:
Vue.set()
这样的 API。
本质原因就是:
defineProperty 无法监听对象结构变化。
Proxy 为什么更适合响应式?
Proxy 不再监听:
某一个属性。
而是:
整个对象。
例如:
const proxy = new Proxy(target,{
get(){},
set(){}
})
此时:
无论:
新增。
删除。
修改。
访问。
都会进入 Proxy。
也就是说:
监听粒度:
从:
Property
升级到了:
Object。
这是 Vue3 最大的变化。
Proxy 可以拦截哪些操作?
很多开发者只知道:
get
set
实际上:
Proxy 支持十几种 Trap。
例如:
get
set
has
deleteProperty
ownKeys
defineProperty
apply
construct
因此:
Vue3 可以监听:
"user" in obj
Object.keys(obj)
delete obj.xxx
这些过去几乎无法优雅实现的操作。
Vue3 响应式核心流程
可以简化理解为:
读取数据
↓
track()
↓
记录依赖
↓
修改数据
↓
trigger()
↓
更新副作用
其中:
track:
负责:
收集依赖。
trigger:
负责:
通知更新。
整个系统围绕这两个核心函数展开。
为什么使用 WeakMap?
Vue3 内部大量使用:
WeakMap
而不是:
Map。
原因:
对象销毁后。
WeakMap:
不会阻止垃圾回收。
否则:
大型项目:
很容易:
内存泄漏。
因此:
依赖关系:
通常:
WeakMap
↓
Map
↓
Set
逐层组织。
既提高查询效率。
也减少内存占用。
Effect 的作用
每个:
computed。
watch。
组件渲染。
最终都会包装成:
Effect。
Effect:
就是:
需要重新执行的一段函数。
当:
trigger()
发生。
对应 Effect:
重新运行。
页面自然更新。
为什么 computed 比 method 更高效?
computed:
拥有缓存。
只有依赖发生变化。
才重新计算。
而 method:
每次渲染都会执行。
对于复杂计算。
computed 更适合。
响应式并不是免费的
每一次:
Proxy。
每一次:
track。
每一次:
trigger。
都会产生开销。
因此:
大型对象。
深层嵌套。
大量响应式数据。
都会影响性能。
对于:
不会变化的数据。
推荐:
使用:
markRaw
shallowRef
shallowReactive
减少响应式成本。
总结
Vue3 使用 Proxy,并不是为了追求新语法。
而是为了构建一个更完整、更统一、更高性能的响应式系统。
相比 Vue2,它不仅解决了新增属性、数组监听等历史问题,也让整个依赖追踪机制更加灵活。
理解响应式原理,不只是帮助我们写出更高效的 Vue 代码,也能够帮助我们理解现代前端框架在状态管理和渲染优化上的设计思路。
X记录空间