vue | vue3响应式与vue2的对比
Vue官方上说:“虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。”
所以我们知道Vue框架有借助MVVM的思想。它一个核心思想就是数据驱动。数据驱动就是指视图是由数据驱动生成的,我们若想让视图改变,要通过修改数据来进行修改,而不是像原始 JS那样直接操作DOM。
响应式?
响应式是一种侦测数据变化的机制。首先了解一下Vue是如何实现数据驱动的,这是一幅Vue官方的配图。
- 黄色那一块是Vue负责渲染的
render function
,当视图初始化以及视图更新时都会调用这个render function
。 - 而每次渲染时都会无法避免地触碰到我们的数据,也就是图中紫色的
data
部分。渲染时通过触发涉及数据的getter
,把涉及数据作为依赖收集到watcher
中。(而且这个watcher
在每个组件实例中都会对应有一个) - 所以在后面我们修改这些收集到的依赖数据时,就会触发数据的
setter
。setter
方法会修改数据的值并通知(notify
)给watcher
,最后watcher
再触发render function
进行视图的重新渲染。
上面说的就是Vue实现数据驱动的过程,可以看出来实现数据驱动的关键是让我们的数据变成响应式,而getter
和setter
这两个方法又是让数据变成响应式的关键。但是我们写在data
中的原始数据并没有对应功能的getter
和setter
啊?其实这就是Vue的工作:重写数据的getter
和setter
。
Vue2
那么Vue是怎么实现数据的响应式的?首先我们来看一下Vue2的实现原理。Vue2主要是借用Object.defineProperty()
来对数据的getter
和setter
进行重写。Object.defineProperty()
的官方描述是:在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
一个简单使用例子:
let person = {}
let name = 'xiaobai'
Object.defineProperty(person, 'personName', {
get: function () {
console.log('触发了get方法')
return name
},
set: function (val) {
console.log('触发了set方法')
name = val
}
})
//当读取person对象的personName属性时,触发get方法
console.log(person.personName)
//当修改name时,重新访问person.personName发现修改成功
name = 'liming'
console.log(person.personName)
// 对person.personName进行修改,触发set方法
person.personName = 'huahua'
console.log(person.personName)
通过这种方法,我们成功监听了person上的name属性的变化。
在这个例子中,我们只监听了一个属性的变化,但是在实际情况中,我们通常需要一次监听多个属性的变化。这时候我们就需要借助Object.keys(obj)
来对对象的所有属性来进行遍历。
但是如果此时只是简单地将这两个API结合,就会有问题:
let person={
name='',
age=0
}
Object.keys(person).forEach(function (key) {
Object.defineProperty(person, key, {
enumerable: true,
configurable: true,
// 默认会传入this
get() {
return person[key]
},
set(val) {
console.log(`对person中的${key}属性进行了修改`)
person[key] = val
// 修改之后可以执行渲染操作
}
})
})
console.log(person.age)
运行之后发现报错了:栈溢出。这是因为当我们读取person身上的属性时,就会触发get方法,返回person[key],但是访问person[key]也会触发get方法,导致递归调用,最终栈溢出。
这时候我们需要设置一个中转Obsever,来让get中return的值并不是直接访问obj[key]。
let person = {
name: '',
age: 0
}
// 实现一个响应式函数
function defineProperty(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`)
return val
},
set(newVal) {
console.log(`${key}属性被修改为${newVal}了`)
val = newVal
}
})
}
// 实现一个遍历函数Observer
function Observer(obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
}
Observer(person)
console.log(person.age)
person.age = 18
console.log(person.age)
现在实现了对象属性的简单监听,但是如果我们想对对象进行深度监听,要怎么实现呢?其实在上述代码的基础上,加上一个递归,就可以轻松实现啦
function defineProperty(obj, key, val) {
//如果某对象的属性也是一个对象,递归进入该对象,进行监听
if(typeof val === 'object'){
observer(val)
}
Object.defineProperty(obj, key, {
get() {
console.log(`访问了${key}属性`)
return val
},
set(newVal) {
console.log(`${key}属性被修改为${newVal}了`)
val = newVal
}
})
}
不过同时也要在observer里面加一个递归停止的条件:
function Observer(obj) {
//如果传入的不是一个对象,return
if (typeof obj !== "object" || obj === null) {
return
}
// for (key in obj) {
Object.keys(obj).forEach((key) => {
defineProperty(obj, key, obj[key])
})
// }
}
目前这样看上去通过Object.defineProperty()
实现响应式已经差不多了,但其实通过Object.defineProperty()
有一些缺陷:
- 由于
Object.defineProperty
本质是监听已存在的属性,所以其实当我们对对象新增或删除属性时无法被检测到。因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用vm.$set
才能保证新增的属性也是响应式的。其本质也是给新增的属性手动监听。 虽然
Object.definePropoerty()
可以做到对数组的监听(不过对push
、unshift
这种会增加数组索引的操作无法检测到),在Vue2中由于对性能的考虑,尤雨溪并没有选择用Object.defineProperty()
来对数组进行监听,而是通过重写Array
上的原型方法来对数组进行监听。如果你知道数组的长度,理论上是可以预先给所有的索引设置 getter/setter 的。但是一来很多场景下你不知道数组的长度,二来,如果是很大的数组,预先加 getter/setter 性能负担较大。
Vue3
可以看到,通过Object.definePorperty()
进行数据监听是比较麻烦的,需要大量的手动处理。这也是为什么在Vue3.0中尤雨溪转而采用Proxy
。我们先来看看Proxy
:
Proxy
用于创建一个对象的代理,从而实现基本操作的拦截和自定义。
可以理解为:可以理解为在对象之前设置一个“拦截”,当监听的对象被访问的时候,都必须经过这层拦截。现在我们来看看Proxy
是怎么解决前面Object.defineProperty()
的一些缺陷的。
不需要遍历属性,提高性能
Object.defineProperty()
需要遍历所有的属性,这就造成了如果vue
对象的data/computed/props
中的数据规模庞大,那么遍历起来就会慢很多。由于Proxy
代理的是整个对象,而不像Object.defineProperty()
一样是对对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。
//定义一个需要代理的对象
let person = {
age: 0,
school: '西电'
}
//定义handler对象
let hander = {
get(obj, key) {
// 如果对象里有这个属性,就返回属性值,如果没有,就返回默认值66
return key in obj ? obj[key] : 66
},
set(obj, key, val) {
obj[key] = val
return true
}
}
//把handler对象传入Proxy
let proxyObj = new Proxy(person, hander)
// 测试get能否拦截成功
console.log(proxyObj.age)//输出0
console.log(proxyObj.school)//输出西电
console.log(proxyObj.name)//输出默认值66
// 测试set能否拦截成功
proxyObj.age = 18
console.log(proxyObj.age)//输出18 修改成功
不需要对对象的新增属性手动监听
这一点也和前面一样,由于Proxy
代理的是整个对象,自然而然也能监听到对象新增删除属性的变化。这样就不用像使用Object.defineProperty()
那样对新增属性手动添加监听了。
在深度监听上性能优化
由于Object.defineProperty()
的深度监听是一次性就全部监听的,而proxy
的深度监听是在真正读取/操作数据的时候才去递归的,这就起到了一个懒加载的作用,这也大大提升了性能。
不需要再对数组的原型方法重写
前面说到由于Object.defineProperty()
无法对新增数组索引的操作进行监听,加上性能的考虑,vue2是通过另外对数组的重写进行操作拦截的。但是proxy可以直接对数组进行拦截,并且不需要一次性对数据全部进行监听,所以也不会造成性能上的太大影响。
Proxy支持多种拦截操作
对比起Object.defineProperty
仅支持两种操作拦截,Proxy
支持的操作拦截显得格外丰富,P它支持13种:
get(target, propKey, receiver)
:拦截对象属性的读取,比如proxy.foo和proxy['foo']。set(target, propKey, value, receiver)
:拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。has(target, propKey)
:拦截propKey in proxy的操作,返回一个布尔值。deleteProperty(target, propKey)
:拦截delete proxy[propKey]的操作,返回一个布尔值。ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。defineProperty(target, propKey, propDesc)
:拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。preventExtensions(target)
:拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target)
:拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。apply(target, object, args)
:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。construct(target, args)
:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
转载自:https://segmentfault.com/a/1190000042055260