一、Object.defineProperty 定义:Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
为什么能实现响应式 通过defineProperty
两个属性,get
及set
属性的 getter 函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
属性的 setter 函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined
下面通过代码展示:
定义一个响应式函数defineReactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 1function update ( ) { 2 app.innerText = obj.foo3 }4 5function defineReactive (obj, key, val ) { 6 Object .defineProperty(obj, key, {7 get ( ) {8 console .log(`get ${key} :${val} ` );9 return val10 },11 set (newVal ) {12 if (newVal !== val) {13 val = newVal14 update()15 }16 }17 })18 }
调用defineReactive
,数据发生变化触发update
方法,实现数据响应式
1 2 3 4 5 1const obj = {} 2defineReactive(obj, 'foo' , '' ) 3setTimeout (()=> { 4 obj.foo = new Date ().toLocaleTimeString()5 },1000 )
在对象存在多个key
情况下,需要进行遍历
1 2 3 4 5 6 7 8 1function observe (obj ) { 2 if (typeof obj !== 'object' || obj == null ) {3 return 4 }5 Object .keys(obj).forEach(key => {6 defineReactive(obj, key, obj[key])7 })8 }
如果存在嵌套对象的情况,还需要在defineReactive
中进行递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1function defineReactive (obj, key, val ) { 2 observe(val)3 Object .defineProperty(obj, key, {4 get ( ) {5 console .log(`get ${key} :${val} ` );6 return val7 },8 set (newVal ) {9 if (newVal !== val) {10 val = newVal11 update()12 }13 }14 })15 }
当给key
赋值为对象的时候,还需要在set
属性中进行递归
1 2 3 4 5 6 1set (newVal ) { 2 if (newVal !== val) {3 observe(newVal) 4 notifyUpdate()5 }6 }
上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题
现在对一个对象进行删除与添加属性操作,无法劫持到
1 2 3 4 5 6 7 1const obj = { 2 foo: "foo" ,3 bar: "bar" 4 }5observe(obj) 6delete obj.foo 7obj.jar = 'xxx'
当我们对一个数组进行监听的时候,并不那么好使了
1 2 3 4 5 6 7 1const arrData = [1 ,2 ,3 ,4 ,5 ]; 2arrData.forEach((val,index )=> { 3 defineProperty(arrData,index,val)4 })5arrData.push() 6arrData.pop() 7arrDate[0 ] = 99
可以看到数据的api
无法劫持到,从而无法实现数据响应式,
所以在Vue2
中,增加了set
、delete
API,并且对数组api
方法进行一个重写
还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题
小结
检测不到对象属性的添加和删除
数组API
方法无法监听到
需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
二、proxy Proxy
的监听是针对一个对象 的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了
在ES6
系列中,我们详细讲解过Proxy
的使用,就不再述说了
下面通过代码进行展示:
定义一个响应式方法reactive
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1 function reactive (obj ) {2 if (typeof obj !== 'object' && obj != null ) {3 return obj4 }5 6 const observed = new Proxy (obj, {7 get (target, key, receiver ) {8 const res = Reflect .get(target, key, receiver)9 console .log(`获取${key} :${res} ` )10 return res11 },12 set (target, key, value, receiver ) {13 const res = Reflect .set(target, key, value, receiver)14 console .log(`设置${key} :${value} ` )15 return res16 },17 deleteProperty (target, key ) {18 const res = Reflect .deleteProperty(target, key)19 console .log(`删除${key} :${res} ` )20 return res21 }22 })23 return observed24 }
测试一下简单数据的操作,发现都能劫持
1 2 3 4 5 6 7 8 9 10 11 1const state = reactive({ 2 foo: 'foo' 3 })4 5state.foo 6 7state.foo = 'fooooooo' 8 9state.dong = 'dong' 10 11delete state.dong
再测试嵌套对象情况,这时候发现就不那么 OK 了
1 2 3 4 5 6 1 const state = reactive({2 bar: { a : 1 }3 })4 5 6 state.bar.a = 10
如果要解决,需要在get
之上再进行一层代理
1 2 3 4 5 6 7 8 9 10 11 12 13 1function reactive (obj ) { 2 if (typeof obj !== 'object' && obj != null ) {3 return obj4 }5 6 const observed = new Proxy (obj, {7 get (target, key, receiver ) {8 const res = Reflect .get(target, key, receiver)9 console .log(`获取${key} :${res} ` )10 return isObject(res) ? reactive(res) : res11 },12 return observed13 }
三、总结 Object.defineProperty
只能遍历对象属性进行劫持
1 2 3 4 5 6 7 8 1function observe (obj ) { 2 if (typeof obj !== 'object' || obj == null ) {3 return 4 }5 Object .keys(obj).forEach(key => {6 defineReactive(obj, key, obj[key])7 })8 }
Proxy
直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到响应式目的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 1function reactive (obj ) { 2 if (typeof obj !== 'object' && obj != null ) {3 return obj4 }5 6 const observed = new Proxy (obj, {7 get (target, key, receiver ) {8 const res = Reflect .get(target, key, receiver)9 console .log(`获取${key} :${res} ` )10 return res11 },12 set (target, key, value, receiver ) {13 const res = Reflect .set(target, key, value, receiver)14 console .log(`设置${key} :${value} ` )15 return res16 },17 deleteProperty (target, key ) {18 const res = Reflect .deleteProperty(target, key)19 console .log(`删除${key} :${res} ` )20 return res21 }22 })23 return observed24 }
Proxy
可以直接监听数组的变化(push
、shift
、splice
)
1 2 3 1const obj = [1 ,2 ,3 ] 2const proxtObj = reactive(obj) 3obj.psuh(4 )
Proxy
有多达13种拦截方法,不限于apply
、ownKeys
、deleteProperty
、has
等等,这是Object.defineProperty
不具备的
正因为defineProperty
自身的缺陷,导致Vue2
在实现响应式过程需要实现其他的方法辅助(如重写数组方法、增加额外set
、delete
方法)
1 2 3 4 5 6 7 8 9 10 11 12 13 1 2const originalProto = Array .prototype 3const arrayProto = Object .create(originalProto) 4 ['push' , 'pop' , 'shift' , 'unshift' , 'splice' , 'reverse' , 'sort' ].forEach(method => {5 arrayProto[method] = function ( ) {6 originalProto[method].apply(this .arguments)7 dep.notice()8 }9 });10 11 12Vue.set(obj,'bar' ,'newbar' ) 13Vue.delete(obj),'bar' )
Proxy
不兼容IE,也没有 polyfill
, defineProperty
能支持到IE9
使用场景
拦截和监视外部对对象的访问
降低函数或类的复杂度
在复杂操作前对操作进行校验或对所需资源进行管理
保障数据类型的准确性
1 2 3 4 5 6 7 8 9 10 11 let numericDataStore={count :0 ,amount :1234 ,total :14 }numericDataStore=new Proxy (numericDataStore,{ set (target,key,value,proxy ) { if (typeof value!='number' ){ throw Error ("属性只能是Number类型" ) } return Reflect .set(target,key,value,proxy) } }); numericDataStore.count="foo" numericDataStore.count=33 ;
实现观察者模式
1 2 3 4 5 6 7 8 9 const queueObservers=new Set ();const observe=fn => queueObservers.add(fn)const observable=obj =>new Proxy (obj,{set});function set (target,key,value,receiver ) { const res=Reflect .set(target,key,value,receiver); queueObservers.forEach(observer => observer()); return res; }
声明一个私有的apiKey,便于api这个对象内部的方法调用,但不希望外部也能够访问api._apiKey
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 let api={ _apiKey :'123456def' } const RESTRICTED=['_apiKey' ]api=new Proxy (api,{ get (target,key,proxy ) { if (RESTRICTED.indexOf(key)>-1 ){ throw Error (`${key} 不可访问` ) } return Reflect .get(target,key,proxy) }, set (target,key,value,proxy ) { if (RESTRICTED.indexOf(key)>-1 ){ throw Error (`${key} 不可修改` ) } return Reflect .set(target,key,value,proxy) } }) console .log(api._apiKey);api._apiKey='12345' ;