Vue2和Vue3响应式原理
前言
- 响应式原理就是指的是MVVM的设计模式的核心,即数据驱动页面,一旦数据改- 变,视图也会跟着改动。
- vue2的响应式原理是由Object.defineProperty()实现的 (数据劫持)
- vue3的响应式原理是由es6中的Proxy所实现的 (数据代理)
vue2 响应式原理
Object.defineProperty()
定义:
直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。(应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。)
原理:
对象类型:通过Object.defineProperty()对属性的读取,修改进行拦截(数据劫持)。通过里面的getter和setter方法,进行查看和数据的修改,通过发布、订阅者模式进行数据与视图的响应式。
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
基本用法:
Object.defineProperty(obj, prop, descriptor), 它接收三个参数
- obj: 要定义属性的对象。它只接收对象
- prop: 要定义或修改的属性的名称或 Symbol 。
- descriptor: 要定义或修改的属性描述符。
descriptor是一个对象,它定义和修改指定的属性,它包含以下的键值,来对原对象进行数据劫持,即对象会执行这里面的逻辑。
configurable、enumerable 、 writable 、 value、get 、 set.
拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false。
属性值和函数的键 value、get 和 set 字段的默认值为 undefined。
目标:
数据变了,视图就要更新。
代码及注解
let oldArrayPrototype = Array.prototype;
let proto = Object.create(oldArrayPrototype);
// 如果要监听的对象是一个数组,我们又该怎么办呢。
//因为我们明确直到Object.defineProperty()的target只能是对象,如果是数组,
//好像Object.defineProperty()无法实现,那我们应该怎么办。
//而在vue2中,当我们改变数组时,视图也会变化,说明我们也要实现数组的响应式。
//这里我们需要换一个思路,我们去重写一下数组原型上的方法,我们把和数组有关的所有api,
//比如push、pop、shift、reserve ...全部重写一遍。
//当我们在执行这些操作的时候,同时把视图更新的操作也完成。这样就可以了。
//这里你可能会问,我们可以去修改数组的源码吗。诶,当然可以。实际上vue2也就是这么操作的。
['push', 'shift', 'unshift'].forEach(method => {
// 函数劫持,重写函数
proto[method] = function () {
updateView()
oldArrayPrototype[method].call(this, ...arguments)
}
})
// 重写数组方法 push shift unshift pop reserve ...
function defineReactive(target, key, value) {
if (typeof value == 'object' && value !== null) {
observer(value)
}
// get()就是简单的取值,直接把这个属性对应的值返回就好了,
//set()就是当我们要修改值得时候,它接收一个参数就是我们修改的新值newValue,将要执行的逻辑,
//我们的目的就是要在修改值得时候将视图更新,于是在这里直接调用updataView()这个函数就好了,
//然后我们把新值赋值上去。这里做了一个小小的优化就是,如果新值刚好等于老值,
//我们就不需要去更新视图。于是最简陋的vue2响应式原理就实现了。
Object.defineProperty(target, key, {
get() {
return value
},
//如果我们要劫持的对象内部嵌套对象,当我们改变hobbies内部的属性的时候,
//视图依然不会更新,这里只能用到递归来解决。
set(newValue) {
if (newValue !== value) {
updateView()
value = newValue
}
}
})
}
// 首先,我们需要有一个观察者,在vue2源码中也有这个,它的目的是用于判断,
//我们要定义或修改的target是不是一个对象。
//如果是对象,我们用Object.defineProperty()对它进行数据劫持。如果不是我们就直接返回target.
function observer(target) { // 观察者
if (typeof target !== 'object' || target == null) {
return target
}
if (Array.isArray(target)) {
// Object.setPrototypeOf(target, proto)
target.__proto__ = proto
}
for (let key in target) {
// defineReactive(target, key, target[key]) ,
//这个方法就是来调用Object.defineProperty的。
//target是我们要定义或修改的对象,key是要修改或定义的属性,target[key]就是这个属性的值。
defineReactive(target, key, target[key])
}
}
function updateView() {
console.log('更新视图');
}
let data = {
name: '老王',
hobbies: {
a: '喝酒',
b: '抽烟'
},
job: ['driver', 'coder', 'cooker']
}
observer(data)
// console.log(dat)
console.log(data.hobbies.a);
data.hobbies.a = '烫头'
console.log(data.hobbies.a);
console.log(data.job)
data.job.push('teacher')
console.log(data.job)
vue3 响应式原理
原理
-
通过Proxy(代理): 拦截对象中任意属性的变化,包括:属性值的读写,属性的增加,属性的删除等。
-
通过Reffect(反射): 对源对象的属性进行操作
new Proxy(data,{
//拦截读取属性值
get(target, prop){
return Reflect.get(target, prop)
},
//拦截设置属性值或添加新属性
set(target, prop, value){
return Reflect.set(target, prop, value)
},
//拦截删除属性
deleteProperty(target, prop){
return Reflect.deleteProperty(target, prop)
}
})
Proxy
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
基本用法
Proxy 接收两个参数,target, handler
- target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
- handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
- handler get set 属性
- get()
get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。 - set()
set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
- get()
这里先提一下WeakMap,WeakSet:
1.传统的Object只能以字符串作为键名,这具有非常大的限制
2.于是有了map数据结构,它可以用任意数据类型作为键名,有get和set方法,用于取值和添加键值对
3.WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。
4.Set是ES6 提供的新的数据结构 。它类似于数组,但是成员的值都是唯一的,没有重复的值。有add和delete方法,用于添加和删除。还有size属性,用于获取长度
5.WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用。
WeakMap,WeakSet都是弱引用(具体是WeakMap中的key是弱引用)。
解释: 一般我们将数据存在堆当中,需要我么手动去把这些数据清除这些数据的引用。将它们设置为null,很容易造成内存泄漏。然而如果我们使用WeakMap或者WeakSet,WeakMap中的键和WeakSet一旦没有其他对象的引用时,他会消失,也就是会自动被垃圾回收自动回收。
原理实现
目标在vue3中,原始类型响应式数据由ref()实现,引用类型响应式数据由reactive()实现。这里我们就来简单实现一下reactive()的原理,来创造一个响应式的对象。
reactive(target): reactive(target)接受一个对象,也可以是函数和数组
function reactive(target) {
// 创建响应式对象
return createReactiveObject(target)
}
它返回一个createReactiveObject(target)方法的执行,createReactiveObject(target)来实现一个响应式对象。
创建简单的响应式对象
function isObject(val) {
return typeof val === 'object' && val !== 'null'
}
function createReactiveObject(target) { // 创建代理后的响应式对象
if (!isObject(target)) { // 如果不是对象,直接返回
return target
}
let baseHandler = {
get(target,key,receiver) { //receiver:被代理后的对象
console.log('获取');
let result = Reflect.get(target,key,receiver)
return result
},
set(target,key,value,receiver) {
console.log('设置');
let res = Reflect.set(target,key,value,receiver)
return res
},
deleteProperty(target,key) {
console.log('删除');
let res = Reflect.deleteProperty(target,key)
return res
}
}
//proxy接收两个参数,
//第一个是一个对象,用isObject()判断一下就行。
//如果是就对它进行代理,如果不是就直接返回。
//第二个参数是一个对象,用来定义一些代理方法
//然后将我们的目标对象和定义的baseHandler对象作为参数传入一个定义的Proxy实例中,并返回这个被代理后的对象。
//这就是我们创建的响应式对象。这也就是一个简单的reactive()的实现。
let observed = new Proxy(target, baseHandler)
return observed
}
为什么要使用Reflect()?
set(target,key,value,receiver),set接受四个参数,依次为目标对象、属性名、属性值和 Proxy代理后的对象,其中最后一个参数可选。当我们使用set时,会把这些参数传进来,就是把这个键对应的值设置到proxy代理后的对象中。也就是说,我们应该receiver.set(target,key,value,receiver)。然而这样会报错,因为不能在proxy内部再调用proxy,上面这样相当于new proxy.set(),会报错。于是我们使用Reflect.set().
什么是Reflect()?
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有:
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
(2) 修改某些Object方法的返回结果,让其变得更合理。
(3) 让Object操作都变成函数行为。
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。
- 这里就是因为不能调用receiver.set(),于是我们使用Reflect()中的方法,其实作用是一样的。
- get(),deletePrpperty(),has()…
- Proxy有十三种属性,都使用了Reflect.
简单的响应式对象可能会出现的问题
1.当取值的时候,如果我们代理的对象,内部还有对象,这个时候我们需要使用递归来多层代理
解决:
get(target,key,receiver) { //receiver:被代理后的对象
console.log('获取');
//取值的时候判断,是否是一个对象,如果是就再此代理,
//不是就直接返回。
//我们取值是一层一层往下取,如果有嵌套的话,会执行多次get操作。
let result = Reflect.get(target,key,receiver)
return isObject(result) ? reactive(result) : result
//递归多层代理,相比于vue2的优势是,vue2默认递归,
//而vue3中,只要不使用就不会递归。
},
2.当设置的时候,如果老值等于新值,那么就不需要去设置;如果设置的时候源target对象不具备这个属性,我么需要把这个属性内置到我们代理后的对象中。
解决:
set(target,key,value,receiver) {
//老值等于新值,我们就没有去执行视图更新的操作,直接进行了set操作,如果不相等,才去做修改
// console.log('设置');
let hadkey = target.hasOwnProperty(key)
let oldValue = target[key]
if(!hadkey) {
console.log('新增');
} else if (oldValue !== value) {
console.log('修改');
}
//如果对象target内部,压根就没有这个属性,我们去设置这个属性的时候,也让它支持这样的操作,去更新视图。
let res = Reflect.set(target,key,value,receiver)
return res
},
3.如果一个对象已经被代理过一次了,那么我们就不再需要去重复代理
let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = new WeakSet() // 代理过的对象:原对象
WeakSet的特性,当一个对象被代理后,如果再此代理,它不会让重复的对象去到WeakSet中,就不会让它再次被代理。用Set也行,不过利用WeakSet会被垃圾回收机制自动回收的特性,性能会更好。我们在代理之前,去判断WeakMap是否存在我们代理的对象,如果存在,就直接返回,不再执行代理。
优化后的响应式对象
// vue3响应式原理
// 2.0需要递归,数据改变length属性是无效的,对象不存在的属性是不能被拦截的
let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = 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
}
let baseHandler = {
get(target,key,receiver) { //receiver:被代理后的对象
console.log('获取');
// receiver.get() ==》 new proxy().get 这会报错,
//也就意味着我们不能直接取到被代理对象上的属性
// 这时候我们需要用到Reflect,这其实也是一个对象,
//它只不过也含有一些明显属于对象上的方法,且和proxy上的方法一一对应
//取值的时候判断,是否是一个对象,如果是就再此代理,
//不是就直接返回。
//我们取值是一层一层往下取,如果有嵌套的话,会执行多次get操作。
let result = Reflect.get(target,key,receiver)
return isObject(result) ? reactive(result) : result
//递归多层代理,相比于vue2的优势是,vue2默认递归,
//而vue3中,只要不使用就不会递归。
},
set(target,key,value,receiver) {
// console.log('设置');
let hadkey = target.hasOwnProperty(key)
let oldValue = target[key]
if(!hadkey) {
console.log('新增');
} else if (oldValue !== value) {
console.log('修改');
}
let res = Reflect.set(target,key,value,receiver)
return res
},
deleteProperty(target,key) {
console.log('删除');
let res = Reflect.deleteProperty(target,key)
return res
}
}
let observed = new Proxy(target, baseHandler)
toProxy.set(target, observed)
toRaw.add(observed,target)
return observed
}
let proxy = reactive({'name': 'wn'})
proxy.sex = 'boy'
console.log(proxy.sex);
// proxy.name
// proxy.name = 'kite'
// delete proxy.name
// proxy.age
// proxy.name = 'kite'
// console.log(proxy.name);
// let proxy = reactive([1,2,3])
// proxy.push(4)
// proxy.length = 5
// console.log(proxy);
// 如果一个对象被代理后了,那么就不再需要再被代理
Vue2、Vue3响应式原理的区别
区别1:
vue3可以把不存在的属性添加到对象中,并且会被proxy的get拦截到,它把不存在的属性,赋值到代理后的对象中,值为undefined.而vue2不会,虽然Object.defineProperty也有这个功能。vue2是把所有已经存在的属性进行了一次遍历和递归。再去拦截。而vue3使用的Proxy,不会把所有的属性进行一次遍历,他只是在需要使用到某个属性的时候才去代理。当然它也需要用到递归。但是vue2,vue3的递归是不一样的,vue2,需要把对象的所有属性,进行递归,vue3是一种按需递归。
区别2:
vue2对数组的操作需要重写数组的方法进行重写,而vue3则可以轻松实现。
总结
- vue2使用Object.defineProperty()实现响应式原理,而vue3使用Proxy()实现。
- 虽然vue2,vue3面对对象嵌套,都需要递归,但vue2是对对象的所有属性进行递归,vue3是按需递归,如果没有使用到内部对象的属性,就不需要递归,性能更好。
- vue2中,对象不存在的属性是不能被拦截的。而vue3可以。
- vue2对数组的实现是重写数组的所有方法,并改变,vue2中,数组的原型来实现,而Proxy则可以轻松实现。而且vue2中改变数组的长度是无效的,无法做到响应式,但vue3可以。