总所周知,vue2
的响应式原理是靠Object.defineProperty
实现。vue3
却是通过Proxy
来实现。那么,它们之间有什么区别呢?这篇文章将通过手撕简易原理来认识其实现原理。
vue2
先上源码
function trackArray(arr) {
const prototype = Array.prototype
const newProto = Object.create(prototype)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach(item => {
newProto[item] = function (...args) {
prototype[item].call(arr, ...args)
console.log('call ', item)
}
})
arr.__proto__ = newProto
}
function track(obj, key, value) {
observe(value) // 需要在此处深度监听
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return value
},
set(newValue) {
if (value === newValue) {
return
}
console.log('set', key)
value = observe(newValue) // 当修改value为对象时, 需要再进一步监听
}
})
}
function observe(obj) {
if (typeof obj !== 'object' || obj === null) { // 只监听引用类型数据
return obj
}
if (Array.isArray(obj)) {
trackArray(obj)
return obj
}
for (let key in obj) {
track(obj, key, obj[key])
}
return obj
}
因为 Object.defineProperty
是属性层级的api。只能拦截某对象的某个属性.所以只能遍历需要监听对象的属性来依次跟踪。所以监听不了新增属性。而Object.defineProperty
api本身无法跟踪对象删除属性。
所以在vue2中,官方提供了$set
和$delete
。
值得一提的是,实现深度监听是一次性递归所有层级,即使还没有使用。
而且对于数组需要额外处理。不然无法跟踪其原型链上的数组api。数组处理思想是:创建一个指向Array.prototype的对象,在这个对象中创建vue2中的那7个api属性。其每个属性都是调用Array.prototype上的api.同时可实现跟踪。注意this指向。最后,将监听的数组使其原型指向这个创建的对象。
几个问题:
1、监听数组时,可以直接在Array.prototype上做修改吗?为什么?
2、track(obj, key, obj[key])
调用track函数时,为什么要传递第三个参数(obj[key])?
3、源码中,下面展示的代码可以交换吗?
//旧
methods.forEach(item => {
newProto[item] = function (...args) {
prototype[item].call(arr, ...args)
console.log('call ', item)
}
})
//新
methods.forEach(item => {
newProto[item] = (...args) => {
prototype[item].call(this, ...args) // arr换成this
console.log('call ', item)
}
})
4、源码中对应的下面代码,深度递归监听observe(value)
放在get()
的返回值上行不行?反正在set(value)
中也存在深度递归监听,不必放在开始?
答:若是放在返回值中,那么在访问引用类型时都会递归到底,从而出现多余的深层次的访问跟踪。那么为什么vue3却没有这种情况呢? proxy只是相当于包裹一层代理,并不会直接访问属性
...
observe(value) // 需要在此处深度监听
Object.defineProperty(obj, key, {
get() {
console.log('get', key)
return value // return observe(value)?
},
...
vue3
老规矩,先上源码
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}
const proxyHandle = {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
if (Reflect.ownKeys(target).includes(key)) {
console.log('get props: ', key)
}
const obervedRes = reactive(result)
return obervedRes
},
set(target, key, value, receiver) {
if (value === target[key]) {
return true
}
const oldKeys = Reflect.ownKeys(target)
if (oldKeys.includes(key)) {
console.log('set: has props: ', key)
} else {
console.log('set: new Props: ', key)
}
// const observedVal = reactive(value)
const result = Reflect.set(target, key, value, receiver)
return result
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log('delete: props')
return res
}
}
return new Proxy(obj, proxyHandle)
}
proxy
代理是针对对象层次的api
。其实现相对也是挺简单的...
几个问题:
为什么在set值时不用深度监听?
总结
vue2中Object.defineProperty
:
- 深度监听在初始化时就一次性递归监听好了
- 无法跟踪到对象增加新值,删除属性
- 数组需要额外处理
vue3中proxy
:
- 深度监听是在获取值的时候才对其监听(按需监听)
- 可以很完备的监听到属性增加和删除,
- 数组可以原生使用api,不需要再对需要监听的api再做处理