Vue 解析 defineProperty 与 Proxy 老说 vue2,数组对象不行,不行在哪? 老说 vue3 上来,这些数组对象的问题都解决了,到底解决了什么?是真的没有问题了吗?
vue2 的 defineProperty 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 interface Props { configurable: boolean ; enumerable: false ; value: any ; writable: boolean ; get?: () => any ; set?:(value )=> void } Object .defineProperties(obj,key, props : Props)
描述符可以有的属性
configurable
enumerable
value
writable
get
set
数据描述符
可以
可以
可以
可以
不可以
不可以
存取描述符
可以
可以
不可以
不可以
可以
可以
example:
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 var data = { name: '张三' , } var p = defineReactive(data)function defineReactive (data ) { var result = data Object .keys(data).forEach((key ) => { var value = data[key] Object .defineProperty(result, key, { enumerable: true , configurable: true , set (val ) { console .log(`set` , key, val) value = val }, get ( ) { console .log(`get` , key, value) return value }, }) }) return result } console .log(p.name)p.name = '李四' console .log(p.name)
console
遗留问题:
Object.defineProperty
只能根据已存在的 key 来修改get
、set
方法,所以当key
不存在的时候没有办法触发get
和set
所以需要强制刷新,使用$set
vue2 数组对象有什么问题 数组可以看成key
为 0,1…的对象,也可以使用Object.defineProperty
改写get
和set
,也就是说vue2
可以监控数组的数据变化,
问题 1. 当新增值的时候,数据的新的 key 没有被Object.defineProperty
改写,也就没有办法触发页面刷新,因此需要用$set
问题 2. 当数组unshift
插入值的时候,会修改整个数组的每个对象,所以要触发多次get
,set
, 如下图
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 var data = ['张三' , '李四' , '王五' ]var p = defineReactive(data)function defineReactive (data ) { var result = data Object .keys(data).forEach((key ) => { var value = data[key] Object .defineProperty(result, key, { enumerable: true , configurable: true , set (val ) { console .log(`set` , key, val) value = val }, get ( ) { console .log(`get` , key, value) return value }, }) }) return result } p.unshift('赵六' )
console
解析:数组将内部对象,依次往后移动一位,最后在开始位置插入赵六
数据,所以需要重复的get
,set
特别,是最后一个值王五
,移动到 3 号位置的时候,3 号位置没有被Object.defineProperty
改写,因此打印出来的数据上 3 号位置没有get
、set
方法,也就没有触发set
,console.dir(p)
后也就没有触发get
打印get 3 王五
vue2 如何处理
node_modules/vue/src/core/observer/array.js
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 35 const methodsToPatch = [ 'push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'sort' , 'reverse' , ] methodsToPatch.forEach(function (method ) { const original = arrayProto[method] def(arrayMethods, method, function mutator (...args ) { const result = original.apply(this , args) const ob = this .__ob__ let inserted switch (method) { case 'push' : case 'unshift' : inserted = args break case 'splice' : inserted = args.slice(2 ) break } if (inserted) ob.observeArray(inserted) ob.dep.notify() return result }) })
检测数组中的方法有没有被添加数据,如果添加了数据,就将添加的数据用Object.defineProperty
改写。 因此也就解决了,push 后的值没有被监听的问题
遗留问题 因为要对对象的所有属性改写set
、get
方法,所以需要递归遍历
数据的元素,所以性能比较差,所以不要在vue
的data
上挂不需要的值。如window
,jquery
等
vue3 的 Proxy 1 2 3 4 5 6 7 8 9 10 11 interface Handler { get: Function set: Function } const p = new Proxy (target:object , handler :Handler)
example
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 var data = { name: '张三' , } var p = defineReactive(data)function defineReactive (data ) { var result = data p = new Proxy (result, { set (obj, prop, value ) { console .log(`set` , prop, value) obj[prop] = value }, get (obj, prop ) { console .log(`get` , prop, obj[prop]) return obj[prop] }, }) return p } p.name = '李四' p.name
console
vue3 解决了 vue2 什么问题 Proxy 不需要遍历 data 的所有 key 修改 key 的 get 和 set,只需要在最上使用 Proxy,就能监听到当前对象 get 和 set。
那么当我们p.name.first="李"
还能触发 set 吗
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 var data = { name: { first: '张' , content: '三' , }, } var p = defineReactive(data)function defineReactive (data ) { var result = data p = new Proxy (result, { set (obj, prop, value ) { console .log(`set` , prop, value) obj[prop] = value }, get (obj, prop ) { console .log(`get` , prop, obj[prop]) return obj[prop] }, }) return p } p.name.first = '李'
console
从 console 可以看出,只触发 data 的 name 的 get 方法,没有触发 set,也就是说 Proxy 只能监听代理数据的子项,不能监听代理数据的孙子项。
怎么办? 由于 Proxy 是在 Data 上监听,那么为什么不能在 get 被触发的时候,将子项也代理,从而达到监听孙子项的目的那
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 var data = { name: { first: '张' , content: '三' , }, } var p = defineReactive(data)function defineReactive (data ) { var result = data p = new Proxy (result, { set (obj, prop, value ) { console .log(`set` , prop, value) obj[prop] = value }, get (obj, prop ) { console .log(`get` , prop, obj[prop]) return defineReactive(obj[prop]) }, }) return p } p.name.first = '李'
console
如图:监听到了 p.name.first 的 set 事件,这样就解决了数据递归遍历的噩运,提高了速度。
vue3 遗留问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var data = ['张三' , '李四' , '王五' ]var p = defineReactive(data)function defineReactive (data ) { var result = data p = new Proxy (result, { set (obj, prop, value ) { console .log(`set` , prop, value) obj[prop] = value }, get (obj, prop ) { console .log(`get` , prop, obj[prop]) return obj[prop] }, }) return p } console .dir(p[1 ])
console
当 data 为数组的时候也可以监听到。但是 unshift
哪
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 var data = ['张三' , '李四' , '王五' ]var p = defineReactive(data)function defineReactive (data ) { var result = data p = new Proxy (result, { set (obj, prop, value ) { console .log(`set` , prop, value) obj[prop] = value return true }, get (obj, prop ) { var val = obj[prop] if (typeof obj[prop] === 'object' ) { val = defineReactive(obj[prop]) } return val }, }) return p } p.unshift('赵六' )
console
由此可见:在 vue3 中使用了 Proxy 也没有解决触发多次 set 事件的问题。
vue3完整示例
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 function reactive (data ) { if (typeof data !== 'object' || data == null ) { return data } const observed = new Proxy (data, { get (target, key, receiver ) { let result = Reflect .get(target, key, receiver) return typeof result !== 'object' ? result : reactive(result) }, set (target, key, value, receiver ) { effective() const ret = Refect.set(target, key, value, receiver) return ret }, deleteProperty (target, key ) { const ret = Refect.deleteProperty(target, key) return ret }, }) return observed }