如果您觉得这篇文章有帮助的话!给个点赞和评论支持下吧,感谢~
作者:前端小王hs
阿里云社区博客专家/清华大学出版社签约作者/csdn百万访问前端博主/B站千粉前端up主
此篇文章是博主于2022年学习《Vue.js设计与实现》时的笔记整理而来
书籍:《Vue.js设计与实现》 作者:霍春阳
本篇博文将在书第5.1节至5.4节的基础上进一步总结所提到的基础概念,附加了测试的代码运行示例,方便正在学习Vue3或想分析Vue3源码的朋友快速阅读
如有帮助,不胜荣幸
5.1 Proxy和Reflect
代理与被代理
5.1节开篇给到了一个信息:Proxy 只能代理对象,无法代理非对象值,例如字符串、布尔值等(原文)
关于什么是代理,可以举个简单的例子,有a
和b
两个对象,如果我们想通过b
去访问a
里的属性,那么这个b
就是代理
可以理解这个b
为中介,而这个b
,就是new Proxy
的proxy
对象,如下代码所示:
const b = new Proxy(a, {
})
对象a
就是被代理对象(a
被b
代理了),位于new Proxy
第一个参数,而(new Proxy
,下同)第二个参数是一个对象,里面包含了如get()
、set()
之类的拦截方法,关于这一点,在之前的笔记中深入理解Vue3.js响应式系统基础逻辑也提到了
代理的作用
代理的作用在于可以拦截一些基本操作,如读取、修改对象等
逻辑也非常简单,原来通过a.foo
可以访问到a
对象里的foo
属性,现在代理了就变成通过b.foo
去访问了,当访问时就会触发第二个参数里设置的如get()
、set()
之类的拦截方法,那么经过这些方法的拦截,就可以进行一些额外的操作,例如前文笔记里提到的响应式
Proxy拦截函数
函数也是对象,那么同理可以把一个函数fn
当作a
变为被代理对象,那么同理,当调用fn
时会触发第二个参数内的拦截方法
apply与call
书中举例了一个使用代理对象去调用fn
的例子,代码如下:
function fn(name) {
return '我是:' + name;
}
// 使用 Proxy 拦截对 fn 的调用
const p = new Proxy(fn, {
apply(target, thisArg, argArray) {
return target.call(thisArg, ...argArray);
}
})
p('123') // 我是123
这里的apply
是proxy
支持的13个拦截方法之一,在阮一峰大佬的ES6中对apply
也有详细的介绍:
apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。
这里的三个参数分别是:
- target:目标对象
- object:目标对象的上下文对象(this)
- args:目标对象的参数数组
回看上面的例子,apply
接收的三个形参的内容分别是:fn
、this
(undefined)、包含123的参数数组
接着返回了target.call(thisArg, ...argArray);
,也就是fn.call(undefined,'123')
,或者说fn('123')
这里的一个问题是,我们知道此时this
指向的是调用者(使用了call
),也就是fn
,那第一个参数thisArg
其实也就没有发挥作用,或者说一直都是undefined
这里其实有个潜在条件就是,使用proxy.apply
的基本场景就是去拦截函数
整个流程其实就是:
- 用
p
代理了fn
- 传入
123
,被apply
拦截 - 在拦截的逻辑里执行
fn('123')
Reflect的作用
Proxy
的方法,在Reflect
都能找到,也就是说Proxy
有get
,Reflect
里也有
Reflect.get(target, name, receiver)
和Reflect.set(target, name, value, receiver)
的作用和Proxy
内的get
、set
相同
书中提到Reflect
的原因是,使用target
即原始对象去完成对属性的读取,无法完成与副作用函数的绑定
我们来分析一下书中的例子,下面是代码:
const obj = {
foo: 1,
get bar() {
return this.foo;
}
};
const p = new Proxy(obj, {
get(target, key) {
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
trigger(target, key);
}
});
effect(() => {
console.log(p.bar);
});
p.foo++;
这段代码的问题是,执行p.foo++
,不会触发副作用函数,这是为什么?
直接看当前的执行逻辑是怎么样的:
- 执行
effect
,那么会输出p.bar
,那么会触发get
拦截 - 在
track
中,target
是obj
,key
是字符串bar
,也就是obj
的bar
会和当前effect
建立联系(这一步不重要) - 返回
target[key]
,那么触发getter
,此时这里的this
是指obj
,也就是最终返回的是obj.foo
给副作用函数
effect(()=>{ obj.foo })
也就是说,匿名函数输出p.bar
,返回的是obj.foo
,那就很好理解了,obj
不是代理对象,在副作用函数里输出obj.foo
不会触发Proxy.get
拦截,自然就不会与副作用函数进行联系了
解决的办法就是使用Reflect.get(target, key, receiver)
代替target[key]
Reflect.get(target, key, receiver)
返回也是obj.foo
,但它的第三个参数receiver
可以指出是谁在调用
那么代码就更新如下:
const p = new Proxy(obj, {
get(target, key,receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
// ...
});
那么现在,就可以把obj.foo
变为p.foo
了
5.2 JavaScript 对象及 Proxy 的工作原理
对象分为两种,常规对象和异质对象
区分常规对象和异质对象的区别在于其内部方法是使用ECMA的哪一种规范决定的,这里不去详细赘述。需要明白的是Proxy
是一个异质对象
如何区分普通对象和函数对象呢?文中提到:对象的实际语义是由对象的内部方法(internal method)指定的(原文)
最简单的一个区分方法是,函数对象有call()
方法
内部方法具有多态性
在书中提到了代理透明性质,也就是如果定义了一个代理对象p
,但是内部没有指定get()
,那么通过p
去访问被代理对象的某个属性,会调用原始对象的内部方法[[Get]]
。这一点在阮一峰的ES6关于proxy.get
一节也有记载
Proxy对象部署的所有内部方法
内部方法 | 处理器函数 | 使用场景 |
---|---|---|
[[GetPrototypeOf]] | getPrototypeOf | 获取对象原型 |
[[SetPrototypeOf]] | setPrototypeOf | 设置对象原型 |
[[IsExtensible]] | isExtensible | 判断对象是否可以新增属性 |
[[PreventExtensions]] | preventExtensions | 阻止对象新增属性 |
[[GetOwnProperty]] | getOwnProperty | 获取对象自有属性的属性描述符 |
[[DefineOwnProperty]] | defineProperty | 定义对象新属性或修改现有属性,返回镀锡 |
[[HasProperty]] | has | 判断对象是否有指定的属性 |
[[Get]] | get | 获取对象的属性值 |
[[Set]] | set | 设置对象的属性值 |
[[Delete]] | deleteProperty | 删除对象的属性 |
[[OwnPropertyKeys]] | ownKeys | 获取对象所有自有属性的键 |
[[Call]] | apply | 调用函数 |
[[OwnPropertyKeys]] | Construct | 创建一个新的实例 |
书上的表格无使用场景,这里加上便于理解
在书上的例子是举例了deleteProperty
,代码如下:
const obj = { foo: 1 }
const p = new Proxy(obj, {
deleteProperty(target, key) {
return Reflect.deleteProperty(target, key)
}
})
console.log(p.foo) // 1
delete p.foo
console.log(p.foo) // undefined
那么需要注意的是deleteProperty
是Proxy
对象即p
的内部方法,只有删除p
的属性时才会被调用,其实就和p
读取obj
属性时才会调用get
一个意思。在上述代码中,上下文环境要删除的是obj.foo
,所以调用了Reflect.deleteProperty
,回想一下,Proxy
的方法和Reflect
里的方法是一样的名字
问题总结
- JS中的代理是什么
- 结合Vue3响应式理解apply和call
- 了解ES6的Reflect在拦截函数中的作用
- 什么是常规对象和异质对象?
- 如何区分普通对象和函数对象
- Proxy对象的内部方法及其使用场景