作为林妹妹的"松弟"(兄弟),指望她自己发挥主观能动性总结下不会的知识点,是不现实的(而在她这周又千里迢迢跑到湖南考试的大时代背景下,更是天方夜谭)。
所以,我只好就勉为其难,补充补充自己的知识盲区了。
希望她看到了以后,有种醍醐灌顶的感觉,正所谓:“听君一席话,如听一席话...”
简述: Proxy和Reflect概要
Proxy和Reflect是ES6推出的两个重要的API,这两个API经常配合使用,他们相当于将以前JS引擎底层的功能暴漏了一部分出来,使得开发者可以使用这些功能。这这一部分,我们首先要搞清的问题就是,这两个东西是干嘛的,为什么要有这些东西。
Proxy
Proxy暴露了在对象上的内部工作,代理是一种封装,能够拦截并改变 JS 引擎的底层操作。 这句话来自Nicholas C. Zakas的《深入理解ES6》这本书(翻译版本)。
这句话值得我们仔细分析,得到了和Proxy有关的几个关键信息:
- 是什么呢:暴露了在对象上的内部工作。
- 在对象上,这个对象,点明了操作对象是object,那么我们就知道,基本类型肯定不行,比如number、string、boolean....这些都不行,不能使用Proxy;那么函数可以吗,当然可以,函数在JS中本质也是对象。
- 内部工作:什么工作是内部工作?我们刚才已经澄清了操作对象是object,那么这里的内部工作就是那些引擎做的事情,比如
obj.age = 12
,这是一个赋值语句,设置obj这个对象,是一种对object操作行为,但是这个设置究竟是怎么设置的?JS程序猿不用关心,交给JS引擎就好了,那么这些交给JS引擎的事情就是所谓“内部工作”。 至于这些“内部工作”都有哪些,后文讨论。
- 用来做什么:代理是一种封装,能够拦截并改变 JS 引擎的底层操作 这句话就就比较好理解了:代理用来拦截JS引擎的底层操作,这里的“底层操作”差不多就是我们上文中“内部工作”,但我理解其实更底层,这不重要。JS引擎的工作被拦截了,我们自然可以在拦截器里面定制一些行为,比如加点逻辑,比如直接返回,拒绝执行等。
上面咬文嚼字的分析完,差不多可以得到一个简练的结论了: 代理就是对象的操作行为拦截器
结论就是这么的爽脆~
对象的操作总结
接着,简单总结下对象操作:我们都能对对象做什么(上文中的“内部工作有哪些”)?
假设存在一个对象let obj = { name : 'yxnne', age: 3 }
,我们在代码中能做什么呢?
- 获得对象属性值
obj.name
- 设置对象属性值
obj.name = 'Muscular Man'
- 判断某个属性是不是存在
console.log('name' in obj) //true
- 删除属性
delete obj.name
- 获取某对象的原型对象
Object.getPrototypeOf(obj)
Object.getPrototypeOf()和obj.__proto__拿到的应该是一致的
- 设置某个对象的原型对象
Object.setPrototypeOf(obj, { favor: 'muscle building' });
- 查看对象是不是可以添加属性
Object.isExtensible(obj)
- 设置对象不可添加属性
Object.preventExtensions(obj)
- 都有哪些属性
Object.getOwnPropertyNames(obj); // ['name', 'age' ]
Object.getOwnPropertySymbols(obj); // []
Object.keys(obj) // ['name', 'age' ]
上面这些方法都能得到属性key对应的数组。Object.getOwnProperyNames() 、Object.keys() 方法会将Symbol类型的属性过滤出去。 Object.getOwnPropertySymbols() 只会返回Symbol类型的属性。
- 定义对象的属性
const defaultSettings = {
writable:true,
enumerable:true,
configurable:true
};
Object.defineProperty(obj, 'hasWife', {
value: true,
...defaultSettings,
})
Object.defineProperties(obj,{
isMarried: {
value: function(){ return this.hasWife },
...defaultSettings,
},
getBasicInfo: {
value:function(){ return `${this.name} / ${this.age} / ${this.favor}` },
...defaultSettings,
}
})
- 获得某个属性的描述对象
Object.getOwnPropertyDescriptor(obj,'name')
得到的name属性的描述对象就是:{value: "yxnne", writable: true, enumerable: true, configurable: true}
- function对象可以执行
fn(); // 执行
fn.call(null, 1, 2, 3)
fn.apply(null, [1, 2, 3])
- function作为构造函数
function Dog(){}
let luDan = new Dog();
Proxy用法
Proxy是一个构造函数,用法就是new Proxy(target, handler)
。(还有一种用法是通过Proxy.revocable()
创建一个可撤销代理,let { proxy, revoke } = Proxy.revocable(target, {});
如果撤销则调用revoke()
,这个不是重点)
Proxy返回了不同于目标target的对象的另外一个对象,称之为代理对象,通常情况下,我们希望对目标对象的操作行为,经过被代理对象拦截。
- target就是目标对象
- handler就是对象操作行为的拦截器,有时候也被称为“陷阱”。
我们再回看上文的总结“代理就是对象的操作行为拦截器。”这句话需要再明晰下,代理其实是另一个对象,而拦截行为是作用于另一个对象自己的,本质上不是被拦截原对象的。 而我们期望作用到原对象身上,所以,代理和原对象之间需要有一定的依赖关系。 举例说吧:
const a = { age: 12 }
const proxy_a = new Proxy(a, {
set(target, k, v, receiver) {
target[k] = v + 1 // 虚岁 +1
}
})
a和proxy_a是两个对象,proxy_a是基于a的代理对象。当设置proxy_a.age的时候,proxy_a拦截自身的set行为,所以走了我们handler中的set方法,在set方法中我们的逻辑是把target的age属性修改(k属性设置赋值为v)。 那其实完全可以在set方法中写其他逻辑,比如:
const proxy_a = new Proxy(a, {
set(target, k, v, receiver) {
proxy_a.age = 0
}
})
虽然几乎没有这样用的,但是不代表不可以。这样的话对proxy_a的赋值就无法作用在a上。因为我们没有使用代理和原对象之间的依赖关系。 话说回来,既然我们通常希望通过代理去拦截对原始对象的操作行为,这个依赖关系是一定要使用的。就是这个参数target,它代表了原始对象。
Reflect
概念
如果你接触过Java,并且对反射的概念先入为主,你可能会感到吃力,因为发现似乎ES6中的Reflect和Java反射不太一样。 在类似Java这种强类型语言中,反射的概念是指运行时动态的加载类,可以动态的获得方法信息,并调用的能力。(正常方法是通过一个类创建对象,反射方法就是通过一个对象找到一个类的信息。) 从这个角度来看,Java的反射倒是有点像JS中Object.getOwnPropertyNames()或者Object.keys()这样的方法。
ES的Reflect要是按照先入为主的Java反射理解就摸不着头脑。
那么,重新理解ES6中的Reflect,在ES6中,Reflect就是针对对象的操作行为的方法,由JS引擎暴露出来的API。比如说,对对象的属性赋值操作,这个行为底层的操作,和你调用Reflect.set()是一致的。
const obj = { name: 'yxnne' }
// 下面两件事其实做了同一件事情
obj.name = 'strong'
Reflect.set(obj, 'name', 'man')
Why Reflect?
上面的概念的解释很平淡,而且例子很奇怪,看上去Reflect并没什么特别的还很麻烦很多余。 那么,为什么是要有Reflect?(查阅了一些资料)
-
将Object对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。目前都这些方法在Reflect和Object都存在。
-
对对象操作都变成函数。类似delete这种命令给他函数调用化:Reflect.deleteProperty(), in 操作符也一样 Reflect.has()
-
Object一些方法返回结果更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而 Reflect.defineProperty(obj, name, desc)则会返回 false。
-
Reflect、 Proxy的方法一一对应。这个可能是最重要的原因了知乎:ES6设计反射Reflect的意义是什么?(除了把key in obj、delete这些方式函数化)? 、 [译]深入理解 ES6 中的反射
Proxy/Reflect API: 十三道峰味
上文中列举了13种对object的操作方法,这些方法对应的,Reflect、 Proxy也都是对应的。 另外,需要说明的是,下文中的每一个方法都存在Proxy和Reflect对应的同名方法(这就是拦截器,也可以称之为陷阱函数),通用的套路就是在Proxy的某陷阱函数中直接return Reflect[某陷阱函数]就相当于什么都没做,JS的默认行为。 那么就简单过一下,看看这十三道峰味吧。
get
get的Proxy、Reflect对应的参数都是3个:
- trapTarget :将接收属性的对象(即代理的目标对象);
- key :需要写入的属性的键(字符串类型或符号类型);
- receiver :操作发生的对象(通常是代理对象)。 下面代码中定义了一个target对象的代理对象proxy,proxy对get操作进行拦截,当试图去读取对象不存在的属性值时,会抛出错误。如果待读取属性是存在的,利用Reflect拿到target的属性值。
let target = { };
let proxy = new Proxy(target, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
我们验证下:
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // 抛出错误
在Nicholas C. Zakas的《深入理解ES6》中提到说:
在多数语言中,试图读取 target.name 属性都会抛出错误,因为该属性并不存在;但 JS 语 言却会使用 undefined 。如果你曾经在大型代码库上进行过工作,那么你可能明白这种行为 会导致严重的问题,尤其是当属性名称存在书写错误时。使用代理进行对象外形验证,可以 帮你从这个错误中拯救出来。
set
set的Proxy、Reflect对应的参数都是4个:
- trapTarget :将接收属性的对象(即代理的目标对象);
- key :需要写入的属性的键(字符串类型或符号类型);
- value :将被写入属性的值;
- receiver :操作发生的对象(通常是代理对象)。 下面代码定义了一个代理,用于对目标target的属性赋值行为拦截验证,其验证的逻辑是,新增属性的话,必须是number类型,不然就报错。
let target = { name: "target" };
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) { // 忽略已有属性,避免影响它们
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
测试:
// 添加一个新属性
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1 // 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误
proxy.anotherName = "proxy";
has
has会在使用 in 运算符的情况下被调用。会被传入两个参数:
- trapTarget :需要读取属性的对象(即代理的目标对象);
- key :需要检查的属性的键(字符串类型或符号类型); 用法和上面的差不多。 下面代码的效果相当于屏蔽了某些属性:
let target = {
name: "target", value: 42
};
let proxy = new Proxy(target, {
has(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.has(trapTarget, key);
}
}
});
console.log("value" in proxy); // false
console.log("name" in proxy); // true
console.log("toString" in proxy); // true
本来value 与 toString 均存在于 object 对象中,因此 in 运算符都会返回 true 。(value 是对象自身的属性,而 toString 则是原型属性(从 Object 对象上继承而来))。但是,我们的代理对象proxy使用has拦截了in操作,在in中屏蔽了value,所以导致结果value in proxy返回的是false。
deleteProperty
deleteProperty 会在使用 delete 运算符去删除对象属性时下被调用,并且会被传 入两个参数:
- trapTarget :需要删除属性的对象(即代理的目标对象);
- key :需要删除的属性的键(字符串类型或符号类型)。
这里需要注意下,delete操作符是有返回的,表示删除成功与否:let result = delete target.name;
比如某个属性设置成不可配置的了,却还去删除,那结果就是false:
Object.defineProperty(target, "name", { configurable: false });
// 试图删除不可配置属性
let result = delete target.name;
console.log(result); // false
Proxy、Reflect的deleteProperty使用举例如下:
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false; // 表示不可删除
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
getPrototypeOf/setPrototypeOf
获取和设置某对象的原型对象,这两个放一起介绍。对应的Object方法是Object.getPrototypeOf() 和 Object.setPrototypeOf()。对应的Proxy和Reflect中的函数就叫getPrototypeOf/setPrototypeOf参数如下:
- trapTarget :需要设置原型的对象(即代理的目标对象);
- proto :需用被用作原型的对象。
还有一些需要注意的点:
- getPrototypeOf 陷阱函数的返回值必须是一个对象或者 null ,其他任何类型会抛出错误;
- setPrototypeOf 必须在操作没有成 功的情况下返回 false
最基本的使用:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
下面这段代码则是利用了代理将目标对象的原型隐藏起来,同时也使它不可修改:
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return null;
},
setPrototypeOf(trapTarget, proto) {
return false;
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // false
console.log(proxyProto); // null
// 成功
Object.setPrototypeOf(target, {});
// 抛出错误
Object.setPrototypeOf(proxy, {});
preventExtensions/isExtensible
可扩展性就是,查看对象是不是可以添加属性,以及阻止对象添加属性。对应的Object方法是:Object.isExtensible()、Object.preventExtensions()
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
defineProperty
Proxy的defineProperty要求你返回一个布尔值用于表示操作是否已成功。 当它返回 true 时,Object.defineProperty() 会正常执行; 返回 false ,则 Object.defineProperty() 会抛出错误; 下面这段代码的作用是,不允许定义Symbol类型的属性:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
参数中 descriptor是一个对象,其可用的属性有:enumerable 、 configurable 、 value 、 writable 、 get 、 set; 当给proxy定义一个Symbol的属性,就会抛错:
let nameSymbol = Symbol("name"); // 抛出错误
Object.defineProperty(proxy, nameSymbol, { value: "proxy" });
getOwnPropertyDescriptor
要求返回值必须是 null 、 undefined ,或者是一个对象。 当返回对象时,只允许该对象拥有 enumerable 、 configurable 、 value 、 writable 、 get 或 set 这些自有属性。如果你返回的对象存在其他属性,则会报错:
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return { name: "proxy" };
}
});
// 抛出错误
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
Object.getOwnPropertyDescriptor() 的第一个参数key如果是基本类型,默认将该参数转换为一个对象。 Reflect.getOwnPropertyDescriptor() 的第一个参数key如果是基本类型的时候抛出错误:
let descriptor1 = Object.getOwnPropertyDescriptor(2, "name");
console.log(descriptor1); // undefined
// 抛出错误
let descriptor2 = Reflect.getOwnPropertyDescriptor(2, "name");
ownKeys
Proxy的ownKeys接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象,不合要求的返回值会导致错误。返回的这个数组会被用于四个方法:
- Object.keys()
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.assign() 方法, Object.assign() 方法会使用该数组来决定哪些属性会被复制。 另外,ownKeys 陷阱函数也能影响 for-in 循环,因为这种循环内部调用了Proxy陷阱函数来决定哪些值能够被用在循环内。 比如又过滤了Symbol属性:
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "symbol";
});
}
});
const sy = Symbol('s')
proxy[sy] = 'my symbol props value'
Object.keys(proxy) // []
apply/construct
函数也是对象,拥有两个内部方法: [[Call]] 与 [[Construct]] ,前者会在函数被直接调用时执行,而后者会在函数被使用 new 运算符调用时执行。使用了函数的代理,会被视为函数。 Proxy和Reflect的apply函数都接受三个参数:
- trapTarget :被执行的函数(即代理的目标对象);
- thisArg :调用过程中函数内部的 this 值;
- argumentsList :被传递给函数的参数数组。
当使用 new 去执行函数时, construct 陷阱函数会被调用并接收到下列两个参数:
- trapTarget :被执行的函数(即代理的目标对象);
- argumentsList :被传递给函数的参数数组。
下面是一个最基本的例子:
let target = function () { return 42 };
let proxy = new Proxy(target, {
apply: function (trapTarget, thisArg, argumentList) {
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function (trapTarget, argumentList) {
return Reflect.construct(trapTarget, argumentList);
}
});
// 使用了函数的代理,其目标对象会被视为函数
console.log(typeof proxy); // "function"
console.log(proxy()); // 42
let instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
当然,具体应用肯定没有这么无聊,后文再说。
应用
校验和过滤
set陷阱校验
set拦截的是赋值行为,这可能是程序中最高频的操作了,所以这也是Proxy/Reflect最根本、最核心、最常用的思路的。本质上你可以在set、get中添加任何你需要的逻辑。 下面代码的作用是在给对象添加新的属性时,限制其值必须是number。
let target = { name: "target" };
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) { // 忽略已有属性,避免影响它们
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("Property must be a number.");
}
}
// 添加属性
return Reflect.set(trapTarget, key, value, receiver);
}
});
当然,这只是很小的一个DEMO,实际应该起来有无限种可能。
校验函数参数
函数参数校验可能过去都放在函数体里面,这很常规,但不见得最好,因为这样导致函数的不相关逻辑会越来越多。 下面的例子使用了代理校验函数参数(只能是number类型),同时限制了这个函数不能做为构造函数使用:
// 将所有参数相加
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
let sumProxy = new Proxy(sum, {
apply: function (trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function (trapTarget, argumentList) {
throw new TypeError("This function can't be called with new.");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// 抛出错误
console.log(sumProxy(1, "2", 3, 4));
// 同样抛出错误
let result = new sumProxy();
以上限制了函数不能作为构造函数,那么对应的,也可以限制为只能作为构造函数使用:
function Numbers(...values) {
this.values = values;
}
let NumbersProxy = new Proxy(Numbers, {
apply: function (trapTarget, thisArg, argumentList) {
throw new TypeError("This function must be called with new.");
},
construct: function (trapTarget, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.construct(trapTarget, argumentList);
}
});
let instance = new NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
// 抛出错误
NumbersProxy(1, 2, 3, 4);
好,那下面这个思路又做了什么事情呢?
apply: function(trapTarget, thisArg, argumentsList) {
return Reflect.construct(trapTarget, argumentsList);
}
答案是:调用函数构造器无需使用new关键字:
let instance = NumbersProxy(1, 2, 3, 4);
console.log(instance.values); // [1,2,3,4]
当然,以上又只是冰山一角了,用法还是有无限可能。
获取属性过滤
写属性的时候教研,那么读属性的时候就可能需要过滤。 比如,我们js程序猿以前在定义私有属性的时候,喜欢用下划线开头:
const linSister = {
gender: 'female',
_age: 27,
}
这是一种曾经的非官方约定,现在还有人保持这个习惯。但是JS引擎并不在乎(主不在乎...),所以,利用Proxy可以扩展功能,将这些私有属性过滤掉:
let proxy = new Proxy({}, {
ownKeys(linSister) {
return Reflect.ownKeys(trapTarget).filter(key => {
return key[0] !== "_";
});
}
});
高级用法
参考这篇文章 Proxy 可以做哪些有意思的事情? 其中管道这个用法感觉挺有意思的,摘录一下:
在最新的 ECMA 提案中,出现了原生的管道操作符 |>,在 RxJS 和 NodeJS 中都有类似的 pipe 概念。 使用 Proxy 也可以实现 pipe 功能,只要使用 get 对属性访问进行拦截就能轻易实现,将访问的方法都放到 stack 数组里面,一旦最后访问了 execute 就返回结果。
const pipe = (value) => {
const stack = [];
const proxy = new Proxy({}, {
get(target, prop) {
if (prop === 'execute') {
return stack.reduce(function (val, fn) {
return fn(val);
}, value);
}
stack.push(window[prop]);
return proxy;
}
})
return proxy;
}
var double = n => n * 2;
var pow = n => n * n;
pipe(3).double.pow.execute;
社区知名用法
vue3数据绑定
众所周知,vue3的MVVM方案基于:Proxy + Weakmap + effect,数据响应这块放弃了vue2的Object.defineProperty。
如果只是简单使用proxy依旧存在问题:
- 属性是对象的引用问题,比如data.arr[3] = 'ccc';这个赋值,只写个proxy能监听到吗?监听不到...
- 和Object.defineProperty一样,对数组的可能操作index的操作,有可能会触发多次方法... 下面通过源码分析,简单的过一下流程,看看如何解决这些问题的。(我自己复习下,一年没看vue了)
vue3中的实现,这里面最重要的就是reactive.ts 和 effect.ts:功能上来说,reactive可以把数据变成响应式数据,返回代理对象,而effect接受一个函数,这个函数会在响应式数据变化的时候被触发, 大致就是这个意思:
const { reactive, effect } = VueReactivity;
const data = { count: 0 };
// 返回一个监听的新数据 proxy
const state = reactive(data);
const fn = () => {
const count = state.count;// ==>get count:fn();
console.log('当前的count:', count);
};
effect(fn);
reactive.ts:
export function reactive(target: object) {
//只读数据,直接返回 readonly ==>只读数据
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
//调用创建响应式数据方法
return createReactiveObject(
target,
false,
mutableHandlers,//不同逻辑的处理情况
mutableCollectionHandlers
)
}
其核心逻辑是调用了createReactiveObject函数, mutableHandlers、mutableCollectionHandlers涉及更细致的具体的代理过程: createReactiveObject代码如下:
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 已经是响应式数据,并且不是只读的 proxy
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 原始数据已经有代理数据,直接找到之前代理后的数据
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 判断数据是否可以被代理,比如VNode对象,component实例等
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 处理代理逻辑,内部有对应的get、set
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
);
//代理之后,直接设置,避免下次重复代理一样的原始数据
proxyMap.set(target, proxy);
//返回数据
return proxy
}
看到在这个函数中,首先先进行了几个判断:target已经是响应式数据吗?已经有代理数据吗?数据是否可以被代理?都通过之后,构建了代理对象,这时候具体的方法:
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
对于集合走collectionHandlers、对于数组和对象,走baseHandlers,重点关注下baseHandlers,这个东西其实是引入进来的mutableHandlers,位于包中baseHandlers.ts文件中,这里才是核心逻辑:
export const mutableHandlers: ProxyHandler<object> = {
get, // get逻辑,收集依赖
set, // set逻辑,发布订阅
deleteProperty,
has,
ownKeys
}
先看get:
const get = /*#__PURE__*/ createGetter();
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
//对特殊的key,做了一层处理,判断对应数据是否什么响应类型数据,只读与响应式。或者获取原始数据
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (
key === ReactiveFlags.RAW &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
) {
return target
}
// 是数组吗?
const targetIsArray = isArray(target)
// 数组则借助此对象; arrayInstrumentations
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
//其他情况,直接获取
const res = Reflect.get(target, key, receiver)
//symbol不处理,直接返回
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
return res
}
// 不是只读数据,开始收集依赖:track
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) { // 如果获取到的值是ref数据,直接返回对应的value情况
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 如果是对象,那就做一个懒代理,避免一开始就深度嵌套
// 这里很有点意思
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
这个get函数就很有点东西了~首先也是先做了一堆判断,然后判断了下是不是数组,数组暂且不表。如果不是数组,Reflect.get(target, key, receiver)通过反射拿到原始对象的值,res,之后,做了两件事情:1)track,收集依赖;2)判断res是不是object; 为什么要判断res是不是object,这里其实就是vue3对于对象嵌套监听不到的问题的答案。
当获取的值res,经过isObject(res)判断之后又是对象,这种情况下对res也代理下,再调用reactive(res),这样解决了对象嵌套监听不到的问题,同时,这个是懒代理,懒在哪里?懒在没有像vue2那样对所有属性就深度遍历,而是对属性真正访问的时候,才去做的这样的代理,这样必然节约计算成本,性能甚好,岂不美哉?
如果是数组怎么做?
// 重写数组,因为数组还是会存在修改一次数据,触发多次修改的情况,直接重写方法
const arrayInstrumentations: Record<string, Function> = {}
// values,可能会触发多次get的情况
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
//获取到数据的原始数据
const arr = toRaw(this)
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + ''); // 收集一次依赖
}
// we run the method using the original args first (which may be reactive)
const res = method.apply(arr, args);// 第一次执行,可能参数是响应式数据的情况
if (res === -1 || res === false) {// 可能是数据被代理的情况,查找原始数据,重新处理
// if that didn't work, run it again using raw values.
return method.apply(arr, args.map(toRaw))
} else {
return res
}
}
})
// 改变数组的情况,可能导致多次get、set,特定处理
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
const method = Array.prototype[key] as any
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
pauseTracking()
const res = method.apply(this, args) // 执行方法
resetTracking()
return res
}
});
和vue2思路是相同的,重新数组方法。但是这个重写的逻辑都是借用原始的方法,const method = Array.prototype[key] as any
,然后拿到原始方法的值 const res = method.apply(this, args)
看下set
const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 获取到原来的值
const oldValue = (target as any)[key]
if (!shallow) {
// 处理对应ref数据的情况
value = toRaw(value)
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value; // ref的时候,需要修改value来触发对应数据的内部逻辑
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 判断这个key是新添加的,还是修改的
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 触发修改逻辑
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// receiver 为Proxy或者继承Proxy的对象,这里需要处理原型链的情况,因为如果原型链继承的也是一个proxy,通过Reflect.set修改原型链上的属性会触发两次setter
if (target === toRaw(receiver)) {
if (!hadKey) {
//trigger 派发通知
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
set的核心逻辑就是要得到被代理对象的值,并且通知值已经产生变化,关注核心逻辑,const result = Reflect.set(target, key, value, receiver),得到result值,通过trigger派发通知(新增key和已存在的key调用trigger不太一样)
总结下来:
- 在get阶段,vue3通过懒代理的方式解决object嵌套问题、通过重写数组方法解决调用数组方法后多次触发的问题;这个阶段涉及track收集依赖;
- 在set阶段,涉及trigger,派发通知的过程;
(贴了这么多代码,好像有点跑题了,说回代理....)
proxy比Object.defineProperty好在哪里呢?Object.defineProperty的机制是对每一个属性进行重写,是从属性的视角来出发做一些动作,但是proxy是从对象本身的角度出发。 Object.defineProperty对于新增属性,那就要重新对这个属性进行监听,但是proxy就不用,因为对象就已经被监听(代理)过了。
微前端中的JS隔离方案
我们知道,微前端方案中要做JS隔离,因为不同项目之间来回切换的话,需要保证一个项目中用到的上下文不被另一个项目所污染,这就是所谓JS隔离。 qiankun中有两种JS隔离方案:快照、代理。 快照的原理就是通过遍历和对比,恢复每一个环境的现场,是比较暴力的方法, 操作的对象直接就是window对象,不太安全。 今天这个主题,重点看下代理:(qiankun中叫:ProxySandbox)
/**
* 基于 Proxy 实现的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet; // 沙箱的独立状态池, 与遗留沙箱的差异点,其它基本方法属性相同
name: string;
proxy: WindowProxy;
type: SandBoxType;
sandboxRunning: boolean;
active(): void;
inactive(): void;
constructor(name: string);
}
var proxy = new Proxy(fakeWindow, {
// 旧版本则是通过 diff 算法还原 window 对象状态快照,子应用之间的状态是隔离的,而父子应用之间 window 对象会有污染
set: function set(target, p, value) {
if (sandboxRunning) {
// @ts-ignore
// 这里没有对window对象产生影响,只是操作updatedValueSet
target[p] = value;
updatedValueSet.add(p);
interceptSystemJsProps(p, value);
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn("[qiankun] Set window.".concat(p.toString(), " while sandbox destroyed or inactive in ").concat(name, "!"));
} // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
ProxySandbox沙箱set值到updatedValueSet,取值也优先命中updatedValueSet,没有的话再看window中存在与否。 相比较而言,ProxySandbox更加完备性,完全隔离了window对象,相对安全。