Vue 框架凭借其简洁的响应式系统在前端开发中获得了广泛的应用。从 Vue 2 到 Vue 3,响应式系统的实现方式经历了重要的变化,Vue 3 引入了 Proxy
和 Reflect
,取代了 Vue 2 中基于 Object.defineProperty
的数据劫持机制。这篇文章将详细解读 Vue2 和 Vue3 响应式原理的不同,结合代码示例,帮助读者理解这些变化带来的优势与改进。
Vue2 响应式原理
在 Vue2 中,响应式系统的核心机制是基于 Object.defineProperty
的数据劫持。虽然 Vue2 实现了对对象属性的响应式追踪,但在处理数组时,Vue2 的响应式机制有一定的局限性,比如直接修改数组的值不会触发响应式更新。
首先,来看一个简单的 Vue2 示例,它展示了 Vue2 在数组劫持方面的局限性:
在上面的代码中,当你点击列表时,this.list[0] = 2
并不会触发视图的更新。为什么直接修改数组的值不会触发响应式?Vue2 只能通过修改数组的方法(例如 push
、pop
)或使用 this.$set
添加新属性来触发响应式更新。
手撸响应式
在 Vue2 中,响应式系统是通过 Object.defineProperty
对对象的每个属性进行数据劫持(getter 和 setter)来实现的。但对于数组,Vue 并不会对每个元素进行单独的 get
/set
拦截。Vue 只能通过重写数组的原型方法(例如 push
、pop
等)来检测数组的变化。因此,当你直接通过索引修改数组的某个元素时,Vue 无法监听到这个变化。
以下是简单模拟 Vue2
中数据劫持的原理代码:
js
let obj = {
a:1,
b:2,
c :{
n:3
},
d:['1','2','3']
}
function updateView() {
console.log('更新视图');
}
let value = obj.a
// 数据劫持
Object.defineProperty(obj,'a',{
get(){ // 取值
return value;
// return obj.a 会无限递归,死循环
},
set(val){ // 设置值
value = val
// obj.a = val 同样也不能
updateView()
}
})
obj.a = 100
console.log(obj.a);
在这个例子中,Object.defineProperty
通过 get
和 set
方法来拦截对属性的访问和修改。
在这里 a
被拦截了,所以对 a
的修改可以更新视图。
在看看下面的示例
js
let obj = {
a:1,
b:2,
c :{
n:3
},
d:['1','2','3']
}
function updateView() {
console.log('更新视图');
}
let value = obj.c
// 数据劫持
Object.defineProperty(obj,'c',{
get(){ // 取值
return value;
},
set(val){ // 设置值
value = val
// obj.a = val 同样也不能
updateView()
}
})
obj.c.n = 4
console.log(obj.c);
会发现没有触发更新视图,但是 c
这个属性的值又修改了,说明 Object.defineProperty()
还是不能去劫持为对象的属性。
因此我们需要遍历整个对象的每个属性,并通过 Object.defineProperty()
为其添加 getter 和 setter。这个过程的递归遍历,确保了对象中嵌套的所有层级都能被劫持,并且可以响应状态变化。
Vue 的响应式系统的核心是一个递归函数,负责观察对象的每个属性。如果对象中的某个属性本身也是对象,则继续递归观察它的子属性。这个函数通常被称为 observer()
,其目的是确保对象的每一层次都能响应数据变化。
js
let obj = {
a:1,
b:2,
c :{
n:3
},
d:['1','2','3']
}
function observer(target){
for(let key in target){
defineReactive(target,key,target[key])
}
}
function defineReactive(target,key,value) {
if(typeof value === 'object' && value !==null){
observer(value)
}
Object.defineProperty(target,key,{
get(){ // 取值
return value;
},
set(newVal){ // 设置值
if(newVal !== value){
value = newVal
updateView()
}
}
})
}
observer(obj)
为对象的属性这种情况处理完了,那回到最开始的,当属性类型为数组时,Vue2 使用了另一种策略:重写数组原型上的方法,例如 push
、pop
等方法来实现对数组的响应式处理。
js
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)
Array.from(['push','shift','pop','unshift']).forEach((method) =>{
// 函数劫持,重写函数
proto[method] = function(){
oldArrayPrototype[method].call(this,...arguments)
updateView()
}
})
function observer(target){
if(Array.isArray(target)){
target.__proto__ = proto // 重写数组原型
return
}
for(let key in target){
defineReactive(target,key,target[key])
}
}
这里主要的改动就是,去新创建数组的响应式原型对象,创建一个新的原型对象去存放数组的原型对象,我们后续对将重写的数组方法都放在了这个新对象上,避免修改了数组的原型对象属性。
然后就是重写 proto对象中的这些数组方法,每次调用时都触发 updateView()
。
完整代码:
js
let obj = {
a:1,
b:2,
c :{
n:3
},
d:['1','2','3']
}
let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)
Array.from(['push','shift','pop','unshift']).forEach((method) =>{
// 函数劫持,重写函数
proto[method] = function(){
oldArrayPrototype[method].call(this,...arguments)
updateView()
}
})
function updateView() {
console.log('更新视图');
}
// 观察者
function observer(target){
if(Array.isArray(target)){
target.__proto__ = proto // 重写数组原型
return
}
for(let key in target){
defineReactive(target,key,target[key])
}
}
function defineReactive(target,key,value) {
if(typeof value === 'object' && value !==null){
observer(value)
}
Object.defineProperty(target,key,{
get(){ // 取值
return value;
},
set(newVal){ // 设置值
if(newVal !== value){
value = newVal
updateView()
}
}
})
}
observer(obj)
obj.d.push('4')
到这里我们已经实现了一个简易的Vue2 响应式原理。
总结
主要是完成了一下三点:
-
重写数组方法: Vue2 通过重写数组的常用方法,如
push
、pop
、shift
、unshift
等,使这些方法在执行后可以触发updateView
函数,更新视图。这种方式并没有对数组的每个元素进行劫持,只是对数组整体操作进行了拦截。 -
递归劫持对象: 对于对象,Vue2 使用递归的方式来劫持每个属性,尤其是深层嵌套的对象。对于数组中的对象元素,也可以通过递归的方式进行劫持。
-
响应式视图更新: 当对象或数组发生变化时,
updateView
函数会被调用,模拟视图的更新过程。这在 Vue2 中是通过虚拟 DOM 和 diff 算法完成的。
Vue3 响应式原理
首先,我们来看一个简单的 Vue 3 响应式示例,通过 Vue 3 的 reactive
API 将数据对象 message
变成响应式的,并且在点击标题 <h2>
时修改 message.msg
的值,来实现数据的双向绑定。
Vue 3 中的 reactive
函数用于将普通的 JavaScript 对象变成 响应式对象 。当我们对 reactive
包装的对象进行修改时,Vue 会自动检测到变化,并更新视图。 再这个示例中,message
是一个通过 reactive
包装的对象,当我们点击 <h2>
标签时,message.msg
被修改,Vue 通过响应式系统自动重新渲染视图。
Vue 3 的响应式系统主要基于 JavaScript 的 Proxy
API 来实现。Proxy
可以拦截对对象的操作(如 get
、set
),从而为 Vue 实现自动追踪对象的变化并触发视图更新。
Proxy
Proxy
是 ES6 引入的新特性,允许你创建一个代理对象,这个对象可以拦截并定义对原始对象的基本操作(例如属性读取、设置、删除等)。Proxy
可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy详细API可参考Proxy - ECMAScript 6入门 (ruanyifeng.com)。
Reflect
此外,还会用到 Reflect
。Reflect
是与 Proxy
一同引入的,它提供了一些与对象操作相关的静态方法,比如 Reflect.get()
和 Reflect.set()
。这些方法和 Proxy
捕获器中的行为一致。使用 Reflect
可以避免我们手动调用目标对象的方法,使代码更简洁和安全。Proxy - ECMAScript 6入门 (ruanyifeng.com)
手撸Vue3 响应式原理
现在我们了解了 Proxy
和 Reflect
的基础知识,接下来可以开始实现一个简化版的响应式系统。Vue 3 的响应式系统核心思想是:通过 Proxy
拦截对象的属性读取和修改,动态跟踪依赖关系,在数据变化时更新视图。
js
function isObject(val){
return typeof val === 'object' && val !== null
}
function reactive(target){
return createReactiveObject(target)
}
function createReactiveObject(target){
if(!isObject(target)) {
return target
}
let baseHandler = {
get(target , key , receiver) {
console.log("读取");
let result = Reflect.get(target,key) // target[key] // 不会无限递归
return isObject(result) ? reactive(result) : result // 按需递归
},
set(target, key , value , receiver) {
console.log("修改");
let result = Reflect.set(target, key , value ,receiver) // 将target 中的key值修改为value
return isObject(result) ? reactive(result) : result
}
}
// 对象代理
let observed = new Proxy(target,baseHandler)
return observed
}
let obj = {
a: 1,
b: 2,
c: {
n: 3
},
d: ['a','b','c']
}
let proxy = reactive(obj)
因为 Proxy
功能很强大,可以支持13种拦截操作,所以我们也更简单的去实现了Vue3的响应式原理,
isObject
函数用于判断 target
是否是对象,只有对象才能被代理。如果传入的不是对象(如基本数据类型),我们直接返回原始值。
接下来就是核心------创建 Proxy
拦截器,
js
let baseHandler = {
get(target, key, receiver) {
console.log("读取");
let result = Reflect.get(target, key) // target[key]
return isObject(result) ? reactive(result) : result // 按需递归
},
set(target, key, value, receiver) {
console.log("修改");
let result = Reflect.set(target, key, value, receiver) // target[key] = value
return isObject(result) ? reactive(result) : result
}
}
这里我们主要定义了 读和改 两个拦截操作:
get
:当读取对象的某个属性时,执行 Reflect.get(target, key)
来获取属性值。如果这个值是对象类型,则递归调用 reactive
,将其转换为响应式对象。将递归放进在 get
中,实现了按需递归,只有用到的为对象的属性,才会去递归,优化了一部分性能。
set
:当修改对象的某个属性时,执行 Reflect.set(target, key, value)
来完成属性的修改。如果新的值是对象类型,同样递归转换为响应式对象。
仅仅是这样还不够,因为我们还漏掉了两种情况,一个是如果原对象已经被代理了,另一个是给被代理的对象代理。无论是哪种都是很浪费性能。因此我们引入了 WeakMap
和 WeakSet
去处理。
WeakMap
只接受对象(null
除外)和作为键名,不接受其他类型的值作为键名,且一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
WeakSet
的成员只能是对象和 Symbol 值,而不能是其他类型的值。WeakSet
中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
代码示例
js
let toProxy = new WeakMap() // 原对象 : 代理对象
let toRow = new WeakSet() // 对象代理
function isObject(val){
return typeof val === 'object' && val !== null
}
function reactive(target){
return createReactiveObject(target)
}
function createReactiveObject(target){
if(!isObject(target)) {
return target
}
// 防止原对象被多次代理
let proxy = toProxy.get(target)
if(proxy) {
return proxy
}
// 防止被代理后的对象,再次被代理
if(toRow.has(target)){
return target;
}
let baseHandler = {
get(target , key , receiver) {
console.log("读取");
let result = Reflect.get(target,key) // target[key] // 不会无限递归
return isObject(result) ? reactive(result) : result // 按需递归
},
set(target, key , value , receiver) {
console.log("修改");
let result = Reflect.set(target, key , value ,receiver) // 将target 中的key值修改为value
return isObject(result) ? reactive(result) : result
}
}
// 对象代理
let observed = new Proxy(target,baseHandler)
toProxy.set(target,observed)
toRow.add(observed,target)
return observed
}
let obj = {
a: 1,
b: 2,
c: {
n: 3
},
d: ['a','b','c']
}
let proxy = reactive(obj)
proxy.e = 8
首先是通过传入的 target
作为键名去 toProxy
查找有无对应的值,如果又就代表 target
之前被代理过了,直接返回toProxy
值就好,每次代理完则 以 { target: observed }
存入 WeakMap
中。然后再去查找 WeakSet
中有无 target
,有则代表 target
已经是被代理过的对象,无需代理,同样每次代理后也要将 target
和 observed
存入。
总结
Vue2
Vue 2 的响应式系统主要基于 Object.defineProperty
,会遍历每个对象的属性,使用 Object.defineProperty
对其进行数据劫持,定义 getter
和 setter
。 当我们读取或修改数据时,会触发 get
或 set
,Vue 通过这些拦截来记录依赖(组件、模板中的绑定),当数据发生变化时,触发依赖更新视图。局限性 是 无法监听数组的变化 :Object.defineProperty
只能对对象属性进行劫持,无法直接监听数组的变动。Vue 2 通过重写数组方法 (如 push
、pop
等)来实现对数组变化的检测。不能监测对象属性的新增或删除 :Vue 2 无法动态监听对象新加的属性或删除的属性,必须使用 Vue.set
和 Vue.delete
来手动触发响应式。
Vue3
Vue 3 的响应式系统则完全基于 Proxy
,它能够拦截几乎所有的操作,解决了 Vue 2 的局限性。Vue 3 使用 Proxy
来代理整个对象,拦截对象的各种操作(包括属性读取、修改、删除、添加等),还避免 Vue 2 中的深度递归监听,同时减少不必要的依赖收集和更新,按需递归。 Proxy
能直接监听数组的索引变化和方法调用(如 push
、pop
等),不需要手动重写数组方法。
Vue 3 的响应式系统通过 Proxy
实现了更高的灵活性和更少的限制,使得框架的整体性能更高,开发体验更好。如果文章对你有帮助,可以点个赞哦😊!