持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
相信不少人学习Vue3
的时候被各种新增的API
吓唬住,比如Ref
与reactive
,其中我认为最核心的就是尤雨溪把实现响应式原理的代码重写了:从原来的Object.defineProperty
改写成为Proxy
方式去实现(当初vue发布的时候是2013年,Proxy还没设计出来呢,Object.defineProperty自然是当时最好的方式)。这篇文章帮你理解Proxy
以及讲讲Reflect
。
代理基础
在红宝书上,介绍代理是目标对象的抽象,目标对象既可以直接被操作,也可以通过代理来操作,但如果直接操作会绕开代理施予的行为
其实理解代理可以跟之前我们所说的Object.defineProperty
关联起来,你可以想象成一个加强版的它(当然这么说只是初步帮我们理解,实际上这么说非常不严谨,如果单单是考虑内部实现的方式来说是可以这么关联的)。按照我的理解来说,代理相当于在你所代理的源对象上罩上一个拦截层,外部对源对象的访问都必须通过这个所谓的拦截层,那么代理的作用就是在该拦截层对外部的访问进行设置。在代理上进行改动会直接反映在源对象上,源对象自身的改变也会牵动着代理的改变,如果看过动漫JOJO
的可以理解成替身与替身使者的感觉。下面举个例子来帮助我们理解:
const person = {
name : "路飞",
age : 18
}
let proxy = new Proxy(person,{
get:function(target,property){
return target[property]
}
})
person.name // 路飞
proxy.name // 路飞
// 改变源对象身上的属性
person.name = "乌索普"
person.name // 乌索普
// 代理的数据也会发生改变
proxy.name // 乌索普
// 通过代理改变数据
proxy.name = "娜美"
// 源对象身上的数据也会发生改变
person.name // 娜美
proxy.name // 娜美
通过代理person
对象我们生成了代理对象proxy
,使用代理对象可以轻松访问到person
里的属性与方法
Proxy的使用
讲完代理的基本概念,我们接下来介绍如何定义和使用Proxy
语法
const p = new Proxy(target,handler)
Proxy
需要通过new
操作符创建一个对象的代理
参数
target
需要代理的源对象,可以是任意类型的对象,可以赋空对象{}
handler
处理程序对象,该参数是一个容器对象,里面用来包含多种捕获器,用来定制拦截行为
捕获器就是在处理程序对象中定义的" 基本操作的拦截器 ",在处理程序对象上可以包含零个或者多个捕获器,每个捕获器对应一种基本操作。每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到源对象之前调用捕获器函数,从而拦截或修改相应的行为。
具体的一些捕获器的方法可以参考阮一峰的ES6入门👉Proxy - ECMAScript 6入门 (ruanyifeng.com)
几乎所有的捕获器都是对标Object
里的方法写的,比如getPrototypeOf(target)
就是用来拦截原生对象的Object.getPrototypeOf(proxy)
,并返回一个对象。
可撤销代理
Proxy
暴露了revocable()
方法,用来撤销代理对象与目标对象的关联。该方法会返回一个对象,结构为{'proxy':proxy,'revoke':revoke}
其中proxy
跟我们正常用new
生成的对象没有啥区别,只是它可以被撤销。
const target = {
foo:"bar"
}
const handler = {
get(){
return 'intercepted'
}
}
const {proxy.revoke} = Proxy.revocable(target,handler)
console.log(proxy.foo) // intercepted
console.log(target.foo) // bar
// 调用撤销方法
revoke()
console.log(proxy.foo) // TypeError
this问题
Proxy
代理目标对象的情况下,目标对象的this
会指向代理,这样可能会引发一些问题:
const wm = new WeakMap()
class Person{
constructor(uid){
wm.set(this,uid)
}
get uid(){
return wm.get(this)
}
set uid(newUid){
wm.set(this,newUid)
}
}
const p = new Person(123)
p.uid // 123
const proxy = new Proxy(p,{})
proxy.uid // undefined
这里显示undefined
主要原因就是我们的WeakMap
里是按照this
作为键的,每一个键都对应一个Person
的实例对象,当我们使用proxy
代理对象的时候,this
指向我们的代理,它会去代理对象里去找uid
,那必然是找不到的。
解决办法也很简单,就是让原来用对象实例作为WeakMap
的键改为用代理对象作为它的键。
const proxyPerson = new Proxy(Person,{})
cosnt p = new proxyPerson(456)
p.uid // 456
反射基础
其实理解反射(Reflect)的时候,我们不应该从他的定义出发,我认为我们需要从它设计的意义着手。相比之前通过使用Object
动态获取和操作自身内容,使用反射会更加合理,那么这个合理正是反射的意义。
从使用场景来看,反射和Object
几乎没有区别,比如我们可以通过反射去获取对象上的属性和方法:
const person = {
name:"小智",
age:18
}
Reflect.get(person,"name") // 小智
上面的例子可以发现,通过反射的方式让操作对象从原来的命令式转变成函数式,函数式的好处就太多了,可以简单看看这篇文章👉说说你对函数式编程的理解?优缺点? - 知乎 (zhihu.com)
在红宝书上列举了使用反射的场景
(1) 反射API并不限于捕获处理程序😅这个稍后再说
(2) 大多数反射API在Object上都有对应的方法,通常Object适用于通用程序,而反射使用于细粒度的对象控制于操作
反射的API其实可以参考代理👉Reflect - ECMAScript 6入门 (ruanyifeng.com)
代理+反射
一般我们在用代理的时候会和反射一起搭配使用,回到上面的例子:
const person = {
name : "路飞",
age : 18
}
let proxy = new Proxy(person,{
get:function(target,property){
// 原先写法
// return target[property]
// 使用反射写法
Reflect.get(target,property)
}
})
其实可以不使用反射,但不是所有捕获器的方法都像get()
那样简单,通过手写所有代码完成逻辑显然是不现实的,处理程序对象中所有可以捕获捕获的方法都有对应的反射。反射API为开发者准备好了样板代码,我们可以用最少的代码修改捕获的方法。
反射使得代码更加合理
开发程序时,如果发生问题,我们更希望知道哪里发生问题,而不是抛出错误,一行一行去调试:
const o ={}
try{
Object.defineProperty(o,'foo','bar')
}catch(e){
console.log('error')
}
使用反射避免了大量的去写try/catch
语句,如果发生问题,会返回false
,这样会让我们的代码更加合理化
Vue响应式式原理实现
最后还是想说一下vue这块,之前看视频学的vue老师着重强调了实现原理,我就简写一个vue的响应式原理
vue2
vue2的话根本上就是用Object.defineProperty()
来实现的
const person = {
name:"神秘人",
age:20
}
// 定义一个空对象
const p = {}
Object.defineProperty(p,"name",{
get(){
console.log('有人访问了name属性')
return person.name
},
set(newName){
console.log('有人修改了name属性')
person.name=newName
}
})
p.name // 神秘人
// 有人访问了name属性
p.name = "xyz" // 有人修改了name属性
person.name // xyz
这样一个简陋的响应式原理就完成了,但是它有个很明显的缺陷,他只能捕获到获得属性与修改属性的行为,对于添加和删除捕获不到
vue3
vue3通过代理反射的方式解决了上面的问题
const person = {
name:"神秘人",
age:20
}
const proxy = new Proxy(person,{
get(target,property){
console.log(`有人访问了${property}属性`)
return Reflect.get(target,property)
},
set(target,property,value){
console.log(`有人修改了${property}属性`)
let result = Reflect.set(target,property,value)
return result
},
deleteProperty(target,property){
console.log(`有人删除了${property}属性`)
return Reflect.deleteProperty(target,property)
}
})
proxy.name // 神秘人
// 有人访问了name属性
proxy.name = "奇人" // 有人修改了name属性
porxy.age = "男" //有人修改了sex属性
delete proxy.name // 有人删除了name属性
// true
通过Proxy
解决了Object.defineProperty()
捕获不到添加与删除的问题,只不过在Proxy
中添加和修改被一同写在了set()
方法中了,对于一些特别的捕获形式会有专门的函数API可以使用
最后
代理与反射是ES6
中非常重要的更新,它在某种程度上也解决了Object
臃肿和不合理性,在以后的发展中Object
里的方法会逐渐的移步到Reflect
中,从Vue3
使用代理反射占据着越来越重要的作用。