浅析vue2与3的数据劫持区别
2024-12-09 11:13:16

两年前在vue3在社区兴起之时,很多社区弄潮儿就在分析2与3源码区别,那时候很多人都以为是个新时代会到来。

可是两年过去了,vue3的新时代依旧在缓慢迭代。

对于大部分业务流选手,大家都不关心源码,大家更期望Vue3的相对于Vue2在工程上的亮点在哪,到底能否更爽快的开发,更好更稳定的替换当前工程。

除非是面试,这种源码级的改动几乎是一个必然的面试题。

很不巧,我现在刚好需要处理面试,于是这里整理一下。

正文

本文默认使用者都用过Vue2和Vue3的,如果您没有看过Vue2的响应式原理,推荐您最好先去了解Vue2的响应式原理,因为本文会直接略过很多内容,如果不清楚相关知识,看起来可能体验会很差。

好了,接下来就开始对V2与V3的数据劫持进行区分。

在正式进入对二者区别的分析前,我们需要先简单了解一下,什么是数据劫持。

数据劫持

在没有进入MVVM框架的时代时候,我们要操作DOM中的数据,就需要自己手动封装一套方法,获取对应节点的DOM,然后修改。

因为这样很麻烦,才诞生了前端时代的MVVM的各种响应式实现方式。

这里不展开细说其他框架的响应式原理,单说Vue这块的响应式,在实现响应式的第一步,就是要进行数据劫持。

数据劫持是Vue数据响应式的核心和基础,通过添加代理来给属性的变化添加额外的操作的方式

Vue2方式

1
Obejct.defineProperty(obj,prop,descriptor)

这个方法可以精确修改对象的属性,decriptor有四个参数,分别是

  • configurable:数据是否可删除,可配置,
  • enumerable:属性是否可枚举,
  • get:一个给属性提供 getter 的方法,如果没有 getter 则为 undefined,
  • set:一个给属性提供 setter 的方法,如果没有 setter 则为 undefined object.defineProperty()通过getter/setter属性对数据进行监听,getter监听访问数据,setter监听修改数据,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var value;
Object.defineProperty(o, 'a', {
get() {
console.log('获取值')
return value
},
set(v) {
console.log('设置值')
value = qqq
}
})
o.a = 'sss'
// 设置值
console.log(o.a)
// 获取值
缺陷
  • 只有getter/setter属性无法监听属性的修改删除,在Vue 2.x赋值对象时,需要对对象进行初始化,否则需要使用$Set()进行设置

  • 无法监听组数的数据变化,数组的长度发生变化的时候无法监听,这也是为什么我们直接根据下标改动数组内容时候会失效,vue2通过处理数组扩展方法(push、pop、shift、unshift、splice、sort、reverse这7个方法),通过这些方法,我们依然是可以对数组进行响应式的数组操作的。

    这个核心原因就是在于遍历数组消耗的性能太大,Vue2如果要做数组的响应式,按照原有的实现方式实在是买椟还珠了。针对这点,Vue2也给出来了一些解决方案,比如$Set()方法解决数组内的数据改变无响应的问题

  • 无法拦截对象属性的多层嵌套。vue 2.x表现,watch对多层对象的监听中会失效,也需要$forceupdate()来更新视图。

Vue3方式

为了解决上面的缺陷,Vue3使用了ES6的proxy方法,直接对源数据进行代理操作。

通过建立一个新的实例对象,才操作原有对象,并且提供13种监听操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Reflect.apply(target,thisArg,args)
Reflect.construct(target,args)
Reflect.get(target,name,receiver)
Reflect.set(target,name,value,receiver)
Reflect.defineProperty(target,name,desc)
Reflect.deleteProperty(target,name)
Reflect.has(target,name)
Reflect.ownKeys(target)
Reflect.isExtensible(target)
Reflect.preventExtensions(target)
Reflect.getOwnPropertyDescriptor(target, name)
Reflect.getPrototypeOf(target)
Reflect.setPrototypeOf(target, prototype)
let obj = new Proxy(arr, {
get: function (target, key, receiver) {
// console.log("获取数组元素" + key);
return Reflect.get(target, key, receiver);
},
set: function (target, key, receiver) {
console.log('设置数组');
return Reflect.set(target, key, receiver);
}
})
// 1. 改变已存在索引的数据
obj[2] = 3
// result: 设置数组
// 2. push,unshift添加数据
obj.push(4)
// result: 设置数组 * 2 (索引和length属性都会触发setter)
// // 3. 直接通过索引添加数组
obj[5] = 5
// result: 设置数组 * 2
// // 4. 删除数组元素
obj.splice(1, 1)

显然Proxy完美的解决了数组的监听检测问题,针对数组添加数据,删除数据的不同方法,代理都能很好的拦截处理。

另外Proxy也很好的解决了深层次嵌套对象的问题。

这里我们综合整理一下proxy的优势

  1. 性能提升:Proxy API 比 defineProperty API 在许多情况下具有更好的性能。Vue2使用 Object.defineProperty 方法来拦截对象属性的访问和修改,但它需要遍历每个属性进行拦截。而 Proxy API 允许拦截整个对象,可以更高效地捕获对对象的访问和修改。
  2. 更全面的拦截能力:Proxy API 提供了更多的拦截方法,比 defineProperty API 更灵活、丰富。它支持拦截目标的各种操作,包括读取、设置、删除、枚举等,甚至还可以拦截函数调用和构造函数实例化。
  3. 更好的数组变化检测:Vue 3.0 使用 Proxy API 改善了数组的变化检测机制。Proxy 可以直接拦截数组的索引访问和修改,使得对数组的变化更容易被监听到,从而提供了更可靠的响应式行为。
  4. 更易于处理嵌套对象:Proxy API 能够递归地拦截对象的嵌套属性,而 defineProperty 无法自动递归处理嵌套对象。这使得在 Vue 3.0 中处理嵌套对象更加简单和方便。
  5. 更好的错误提示:相比于 defineProperty,Proxy API 提供了更好的错误追踪和调试信息。当使用 Proxy API 时,如果访问或修改了一个不存在的属性,会直接抛出错误,从而更容易发现和修复问题。

不过,如果硬要挑刺的话,总是能找到缺点的。

比如proxy属性毕竟是ES6的特性,如果有人要兼容低版本的IE10之类的说法,可能vue3这套响应式就不是很能玩得转了。

但是大多情况下,在现代浏览器环境中,V3的proxy代理确实是比V2的Object.defineProperty()实现方式更还好用

reactive与ref

我们在日常的工作中,估计已经非常熟悉二者的使用了,相对于Vue2中用data()声明之后即可简单实现响应式,Vue3提供的这两种方式对于Vue2的习惯用户来说简直是一种致命缺陷。

关于ref和reactive的使用方式优化,在前文浅析ref与reactvie的区别一文中,我给出了一些相对好用的使用方案,至少能让开发者在开发项目的过程中有更好的开发体验。

不过,今天这次不再是简单的讲二者的使用,而是更深层的剖析他们的实现方式。

reactive的缺陷
  1. reactive只能用来处理引用类数据的响应式,不能处理基础类型的数据,这个是官方自己这么规定的且proxy也只适合代理对象罢了。
  2. reactive声明的数据,解构赋值会导致响应式失效
  3. 引用式数据直接赋值会导致响应式失效,比如用reactive代理数组,代理对象,如果你直接赋值一个新数组或者新对象,都会导致响应式失效

以上缺陷几乎是我们日常开发中经常遇到的问题,稍微有点经验的开发老手大概都能猜出来什么原因。

很简单,深拷贝改变了引用类型数据的位置,而引用类的数据都是要根据存储位置的指针去找源数据的存储地址的。

而引用类型数据直接赋值,会直接改变指存储地址,这个重新赋值的过程丢失了响应式自然不奇怪。

解构赋值也是同理,我们用解构赋值处理引用类型数据,也会改变数据存储位置,原理同上,所以这两种情况都会导致响应式失效。

ref的本质

很多开发者在开发中都非常喜欢ref一把梭,虽然这都需要.value`这个小尾巴去获取实际内容。

相对于reactive,ref不需要考虑数据类型,无论是引用类型和基础类型的数据都能用ref代理。

但,ref的本质实际上就是reactive。

很多新人朋友可能在这个过程可能会有点犯迷,为什么ref的本质是reactive?

ref不是比reactive多了一个基础数据类型代理吗?

既然ref既能处理基础数据类型,又能处理引用数据类型,为什么不用ref一把梭?

这里我们放一段在vue工程中随手声明的测试变量,看看这个输出结果吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const test = ref('test')
console.log(test)

// 以下是基础类型test的输出结果:
{
__v_isRef: true
__v_isShallow: false
_rawValue: "test"
_value: "test"
value: "test"
}

const testObj = ref({ a: test })
// 以下是引用类型数据testObj的输出结果:
{
dep: undefined
__v_isRef: true
__v_isShallow: false
_rawValue: {name: 'test'}
_value: Proxy(Object) {name: 'test'}
value: (...)
[[Prototype]]: Object
}

这个复杂对象会有__v_isRef,__v_isShallow等属性,引用类型。

而我们将reactive包裹的对象输出,就没有该属性。

再结合上述示例的输出结果我们就能明白,ref实际上在处理基础数据的时,就是用普通的处理,将基础类型数据转换为一个.value的对象来完成响应式的处理。

ref在处理引用类型数据时,通过输出结果,我们发现了proxy代理的数据,这和reactive输出的结果一模一样的。

显然,ref在处理对象的时候,本质上还是调用reactive。

根据__v_isRef的值去判断,如果是简单的数据,就有ref去处理,如果是复杂的数据,本质还是用reactive去代理。

所以,在处理引用类型数据时候,本质上还是reactive

结语

好了,本次简单的说完了V2与V3的数据劫持方式的不同,不过我水平浅薄,没敢放自己对源码的解读,所以显得有点low。

不过,我尽可能保证自己说的内容都是查证和实验过了,所以准确性还是有保证的。

后续会持续整理更新V2与V3之后,也算是对得起自己这多年前端的经验。

如果您有更好的见解,欢迎在评论区留言,我会参考修正自己的认知。

参考

Vue 2.x和Vue 3.x 数据劫持的实现和优缺点

【Vue中修改了数组数值,为什么界面没有更新?】