value是对象自身的属性,而toString则是原型属性,可以使用代理的has()陷阱函数来拦截这个操作,从而在使用in运算符时返回不同的结果。
将代理用作类的原型
function NoSuchProperty() {
}
let proxy = new Proxy({}, {
get (trapTarget, key, receiver) {
throw new ReferenceError(`${key} doesn't exist`);
}
});
NoSuchProperty.prototype = proxy;
class Square extends NoSuchProperty {
constructor(length, width) {
super();
this.length = length;
this.width = width;
}
}
let shape = new Square(2, 6);
let shapeProto = Object.getPrototypeOf(shape);
console.log(shapeProto === proxy); // false
let secondLevelProto = Object.getPrototypeOf(shapeProto);
console.log(secondLevelProto === proxy); // true
let area1 = shape.length * shape.width;
console.log(area1); // 12
// 由于 wdth 不存在,抛出错误:
// Uncaught ReferenceError: wdth doesn't exist
let area2 = shape.length * shape.wdth;
可以使用ownKeys()陷阱函数去过滤特定的属性,以避免这些属性被Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()或Object.assign()方法使用。
可调用的类构造函数
使用 apply 陷阱创建实例
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("JiaJia");
console.log(me.name); // JiaJia
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
陷阱函数 has
解决数组问题
由于 Vue 3 的变更检测是基于 Proxy 代理的,所以在理解 Vue 3 的响应系统之前,有必要先熟知 Proxy 具有哪些特性和它能解决什么问题。
用deleteProperty陷阱防止删除属性
delete操作符可以从对象中删除属性,如果成功则返回true,不成功则返回false。
在严格模式下,如果你尝试删除一个不可配置(nonconfigurable)属性则会导致程序抛出错误,而在非严格模式下只是返回false。
每当通过delete操作符删除对象属性时,deleteProperty陷阱都会被调用,它接受2个参数:
- trapTarget
要删除属性的对象(代理的目标) - key
要删除的属性键(字符串或Symbol)
Reflect.deleteProperty()方法为deleteProperty陷阱提供默认实现,并且接受同样的两个参数。
let target = {
name: "target",
value: 42
};
let proxy = new Proxy(target, {
deleteProperty(trapTarget, key) {
if (key === "value") {
return false;
} else {
return Reflect.deleteProperty(trapTarget, key);
}
}
});
console.log("value" in proxy); // true
let result1 = delete proxy.value;
console.log(result1); // false
console.log("value" in proxy); // true
console.log("name" in proxy); // true
let result2 = delete proxy.name;
console.log(result2); // true
console.log("name" in proxy); // false
因此在输出结果中_name属性则始终没有出现在结果里,因为它被过滤了。
验证函数参数
例:验证所有参数必须是数字:
// 将所有参数相加
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("所有参数必须是数字");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
},
construct: function(trapTarget, argumentList) {
throw new TypeError("该函数不可通过new来调用");
}
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// 抛出错误
// Uncaught TypeError: 所有参数必须是数字
console.log(sumProxy(1, "2", 3, 4));
// 抛出错误
// Uncaught TypeError: 该函数不可通过new来调用
let result = new sumProxy();
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
使用set陷阱验证属性
set陷阱接受4个参数:
- trapTarget
用于接收属性(代理的目标)的对象 - key
要写入的属性键(字符串或Symbol类型) - value
被写入属性的值 - receiver
操作发生的对象(通常是代理)
Reflect.set()是set陷阱对应的反射方法和默认特性,它和set陷阱一样也接受同样的4个参数。
let target = {
name: "target"
};
let proxy = new Proxy(target, {
set(trapTarget, key, value, receiver) {
if (!trapTarget.hasOwnProperty(key)) {
if (isNaN(value)) {
throw new TypeError("属性必须是数字");
}
}
return Reflect.set(trapTarget, key, value, receiver);
}
});
proxy.count = 1;
console.log(proxy.count); // 1
console.log(target.count); // 1
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
console.log(target.name); // "proxy"
// 抛出错误:
// Uncaught TypeError: 属性必须是数字
proxy.anotherName = "proxy";
in运算符用于判断指定对象中是否存在某个属性,如果对象的属性名与指定的字符串或符号值相匹配,那么in运算符应当返回true,无论该属性是对象自身的属性还是其原型的属性。例如:
使用has陷阱隐藏已有属性
可以用in操作符来检测给定对象中是否含有某个属性,如果自由属性或原型属性匹配的名称或Symbol就返回true。
let target = {
value: 42
};
console.log("value" in target); // true
console.log("toString" in target); // true
在代理中使用has陷阱可以拦截这些in操作并返回一个不同的值。
in陷阱接受2个参数:
- trapTarget
读取属性的对象(代理的目标) key
要检查的属性值(字符串或Symbol)let target = {
name: "target", vlaue: 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
// 这里就没什么好说的function deleteProperty(target, key) { const hadKey = hasOwn(target, key); const oldValue = target[key]; const result = Reflect.deleteProperty(target, key); if (result hadKey) { /* istanbul ignore else */ { 发布通知 trigger(target, "delete" /* DELETE */ , key, { oldValue }); } } return result;}
在原型上使用 set 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
set (trapTarget, key, value, receiver) {
return Reflect.set(trapTarget, key, value, receiver);
}
}));
console.log(thing.hasOwnProperty("name")); // false
// 触发set陷阱
thing.name = "thing";
console.log(thing.name); // "thing"
console.log(thing.hasOwnProperty("name")); // true
// 不触发set陷阱
thing.name = "boo";
console.log(thing.name); // "boo"
ownKeys()代理陷阱拦截了内部方法[[OwnPropertyKeys]],并允许你返回一个数组用于重写该行为。
用get陷阱验证对象结构(Object Shape)
get陷阱接受3个参数:
- trapTarget
被读取属性的源的对象(代理的目标) - key
被读取的属性键 - value
操作发生的对象(通常是代理)
Reflect.get()也接受同样3个参数并返回属性的默认值。
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("属性" + key + "不存在");
}
return Reflect.get(trapTarget, key, receiver);
}
});
proxy.name = "proxy";
console.log(proxy.name); // "proxy"
// 抛出错误:
// Uncaught TypeError: 属性nme不存在
console.log(proxy.nme);
我们向reactive()函数传入了一个{name: "Vue 3.x", count: {…}},对象,reactive()函数会将传入的对象进行 Proxy 封装,将其转换为"可观测"的对象。
创建一个简单的代理
Proxy构造函数有两个参数
- 目标(target)
处理程序(handler)
处理程序是定义一个或多个陷阱的对象,在代理中,出了专门为操作定义的陷阱外,其余操作均使用默认特性。
不使用任何陷阱的处理程序等价于简单的转发代理。let target = {}; let proxy = new Proxy(target, {});
proxy.name = "proxy"; console.log(proxy.name); // "proxy" console.log(target.name); // "proxy"
target.name = "target"; console.log(proxy.name); // "target" console.log(target.name); // "target"
返回原始对象的响应式Proxy代理( 同 2.x 的 Vue.observate() )。
数组问题
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // black
colors.length = 2;
假设你想要创建一个对象,并要求其属性值只能是数值,并且在属性值不为数值类型时应当抛出错误。
可撤销代理
可以使用 Proxy.revocable() 方法创建可撤销的代理,该方法采用与 Proxy
构造函数相同的参数:目标对象和代理处理程序。
返回值是具有以下属性的对象:
- proxy
可被撤销的代理对象 revoke
撤销代理要调用的函数let target = {
name: "target"
};
let { proxy, revoke } = Proxy.revocable(target, {});
console.log(proxy.name); // target
revoke();
// 抛出错误 // Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked console.log(proxy.name);
target:将会被读取属性的对象(即代理的目标对象);key:需要读取的属性的键(字符串类型或符号类型);receiver:操作发生的对象(通常是代理对象)。
原型代理陷阱
ES6中新增的Object.setPrototypeOf()方法,它被用于作为ES5中的Object.getPrototype()方法的补充。通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截这两个方法的执行过程。
setPrototypeOf陷阱接受2个参数:
- trapTarget
接受原型设置的对象(代理的目标) - proto
作为原型使用的对象
传入Object.setPrototypeOf()方法和Reflect.setPrototypeOf()方法的均是以上两个参数。
getPrototypeOf陷阱、Object.getPrototypeOf()方法和Reflect.getPrototypeOf()方法只接受参数trapTarget。
在 Vue 3 中,将 Vue 的核心功能(例如创建和观察响应状态)公开为独立功能,例如使用reactive()创建一个响应状态:
将代理作为原型
let target = {};
let proxy = new Proxy(target, {
defineProperty(trapTarget, name, descriptor) {
return false;
}
});
let newTarget = Object.create(proxy);
Object.defineProperty(newTarget, "name", {
value: "newTarget"
});
console.log(newTarget.name); // "newTarget"
console.log(newTarget.hasOwnProperty("name")); // true
console.log(Object.getPrototypeOf(newTarget) === proxy); // true
关于 Object.create() 方法可以参照 这里
上例中 newTarget 的原型是代理,但是在对象上定义属性的操作不需要操作对象原型,所以没有触发代理中的陷阱。
尽管代理作为原型使用时及其受限,但有几个陷阱仍然有用。
可以使用set()陷阱函数来重写设置属性值时的默认行为,该陷阱函数能接受四个参数:
两个基础示例
默认实现:
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return Reflect.preventExtensions(trapTarget);
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // false
console.log(Object.isExtensible(proxy)); // false
使用陷阱使 Object.preventExtensions() 对 proxy 失效。
let target = {};
let proxy = new Proxy(target, {
isExtensible(trapTarget) {
return Reflect.isExtensible(trapTarget);
},
preventExtensions(trapTarget) {
return false;
}
});
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
// 抛出错误:
// Uncaught TypeError: 'preventExtensions' on proxy: trap returned falsish
Object.preventExtensions(proxy);
console.log(Object.isExtensible(target)); // true
console.log(Object.isExtensible(proxy)); // true
从打印的结果我们可以得知,被代理的目标对象target设置了get()、set()、deleteProperty()、has()、ownKeys(),这几个陷阱函数,结合我们上文介绍的内容,一起来看下它们都做了什么。
代理(Proxy)是一种可以拦截并改变底层JavaScript引擎操作的包装器,在新语言中通过它暴露内部运作的对象,从而让开发者可以创建内建的对象。
// get() = createGetter(false)function createGetter(isReadonly: boolean, unwrap: boolean = true) { return function get(target: object, key: string | symbol, receiver: object) { // 恢复默认行为 let res = Reflect.get(target, key, receiver) // 根据目标对象 key 类型进行的一些处理 if (isSymbol(key) builtInSymbols.has(key)) { return res } // 如果目标对象存在使用 ref 创建的数据,直接获取内部值 if (unwrap isRef(res)) { res = res.value // 案例中 这里是 42 } else { // 调用 track() 方法 track(target, OperationTypes.GET, key) } return isObject(res) ? isReadonly ? readonly(res) : reactive(res) : res }}
在原型上使用 get 陷阱
let target = {};
let thing = Object.create(new Proxy(target, {
get(trapTarget, key, value) {
throw new ReferenceError(`${key} deesn't exist`);
}
}));
thing.name = "thing";
console.log(thing.name); // "thing"
// 抛出异常:
// Uncaught ReferenceError: unknown deesn't exist
let unknown = thing.unknown;
访问对象上不存在的属性时,会触发原型中的 get 陷阱。
截止目前,Vue 3.0 主要的架构改进、优化和新功能均已完成,剩下的主要任务是完成一些 Vue 2 现有功能的移植。
defineProperty 陷阱
defineProperty 陷阱的描述对象已规范化,只有下列属性会被传递给 defineProperty 陷阱的描述符对象。
- enumerable
- configurable
- value
- writable
- get
set
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) { console.log(descriptor.value); // "proxy" console.log(descriptor.name); // undefined return Reflect.defineProperty(trapTarget, key, descriptor); }
});
Object.defineProperty(proxy, "name", {
value: "proxy", name: "custom"
});
陷阱函数
函数代理的 apply 和 construct 陷阱
所有代理陷阱中,只有 apply 和 construct 的代理目标是一个函数。
函数有两个内部方法 [[Call]] 和 [[Construct]],apply 陷阱和
construct 陷阱可以覆写这些内部方法。
若使用 new 操作符调用函数,则执行 [[Construct]] 方法;若不用,则执行
[[Call]] 方法。
apply 陷阱和 Reflect.apply() 都接受以下参数:
- trapTarget
被执行的函数(代理的目标) - thisArg
函数被调用时内部this的值 - argumentList
传递给函数的参数数组
当使用 new 调用函数时调用的 construct 陷阱接受以下参数:
- trapTarget
被执行的函数(代理的目标) - argumentList
传递给函数的参数数组
Reflect.construct() 方法也接受这两个参数,其还有一个可选的第三个参数 newTarget。
let target = function() { return 42; },
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
var instance = new proxy();
console.log(instance instanceof proxy); // true
console.log(instance instanceof target); // true
// runtime-core src apiOptions.tsinstance.data = reactive(data)
属性描述符陷阱
ECMAScript 5 最重要的特性之一是可以使用 Object.defineProperty()
方法定义属性特性(property attribute)。可以通过
Object.getOwnPropertyDescriptor() 方法来获取这些属性。
在代理中可以分别用 defineProperty 陷阱和 getOwnPropertyDescriptor
陷阱拦截 Object.defineProperty() 方法和
Object.getOwnPropertyDescriptor() 方法的调用。
defineProperty 陷阱接受以下参数:
- trapTarget
要定义属性的对象(代理的目标) - key
属性的键(字符串或Symbol) - descriptor
属性的描述符对象
操作成功后返回 true,否则返回 false。
getOwnPropertyDescriptor 陷阱接受以下参数:
- trapTarget
要定义属性的对象(代理的目标) - key
属性的键(字符串或Symbol)
最终返回描述符。
陷阱默认行为示例:
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
return Reflect.defineProperty(trapTarget, key, descriptor);
},
getOwnPropertyDescriptor(trapTarget, key) {
return Reflect.getOwnPropertyDescriptor(trapTarget, key);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
console.log(descriptor.value); // "proxy"
ES6 为了让开发者能进一步接近 JS 引擎的能力,推出了 Proxy,代理是一种封装,能够拦截并改变 JS 引擎的底层操作。简单的说,就是在目标对象上架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,提供了一种改变 JS 引擎过滤和改写的能力。
在原型上使用 has 陷阱
let target = {}
let thing = Object.create(new Proxy(target, {
has (trapTarget, key) {
return Reflect.has(trapTarget, key);
}
}));
// 触发 has 陷阱
console.log("name" in thing); // false
thing.name = "thing";
// 不触发 has 陷阱
console.log("name" in thing); // true
第一次 in 操作符触发 has 陷阱,是因为 name 不是 thing 的自有属性。
target:将接收属性的对象(即代理的目标对象);key:需要写入的属性的键(字符串类型或符号类型);value:将被写入属性的值;receiver:操作发生的对象(通常是代理对象)。
代理和反射
调用 new Proxy() 可创建代替其他目标(taget)对象的代理,它虚拟化了目标,所以二者看起来功能一致。
代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。
反射API可以Reflect对象的形式出现,对象中方法的默认特性与相同的底层操作一致,而代理可以覆写这些操作,每个代理陷阱对应一个命名和参数都相同的Reflect方法。
代理陷阱 | 覆写的特性 | 默认特性 |
---|---|---|
get | 读取一个属性值 | Reflect.get() |
set | 写入一个属性值 | Reflect.set() |
has | in 操作符 | Reflect.has() |
deleteProperty | delete 操作符 | Reflect.deleteProperty() |
getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() |
setPrototypeOf | Object.setPrototypeOf() | Reflect.setPrototypeOf() |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineProperty | Object.defineProperty() | Reflect.defineProperty() |
ownKeys | Object.keys()、Object.getOwnPropertyNames()和Object.getOwnPropertySymbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 用new调用一个函数 | Reflect.construct() |
let target = { name: "target", value: 42}let proxy = new Proxy(target, { has(target, key) { // 拦截操作 if (key === "value") { return false; } else { // 保持默认行为 return Reflect.has(target, key); } }})console.log("value" in proxy); // falseconsole.log("name" in proxy); // trueconsole.log("toString" in proxy); // true
给 Object.defineProperty() 添加限制
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);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 抛出错误:
// Uncaught TypeError: 'defineProperty' on proxy: trap returned falsish for property 'Symbol(name)'
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
});
Reflect.get()方法同样接收这三个参数,并且默认会返回属性的值。
ownKeys 陷阱
ownKeys 陷阱可以拦截内部方法
[[OwnPropertyKeys]],通过返回一个数组的值可以覆写其行为。
这个数组被用于
Object.keys()、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()
和 Object.assign() 4个方法,Object.assign()
方法用数组来确定需要复制的属性。
ownKeys 陷阱通过 Reflect.ownKeys()
方法实现默认的行为,返回的数组中包含所有自有属性的键名,字符串类型和
Symbol 类型的都包含在内。
Object.getOwnPropertyNames() 方法和 Object.keys() 方法返回的结果将
Symbol 类型的属性名排除在外;
Object.getOwnPropertySymbols()
方法返回的结果将字符串类型的属性名排除在外;
Object.assign() 方法支持字符串和 Symbol 两种类型。
ownKeys 陷阱唯一接受的参数时操作的目标,返回值必须是一个数组或类数组对象,否则就抛出错误。
例:过滤任何以下划线字符开头的属性名称。
let proxy = new Proxy({}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => {
return typeof key !== "string" || key[0] !== "_";
});
}
});
let nameSymbol = Symbol("name");
proxy.name = "proxy";
proxy._name = "private";
proxy[nameSymbol] = "symbol";
let names = Object.getOwnPropertyNames(proxy),
keys = Object.keys(proxy),
symbols = Object.getOwnPropertySymbols(proxy);
console.log(names.length); // 1
console.log(names[0]); // "name"
console.log(keys.length); // 1
console.log(keys[0]); // "name"
console.log(symbols.length); // 1
console.log(symbols[0]); // "Symbol(name)"
陷阱函数 get
getOwnPropertyDescriptor 陷阱
getOwnPropertyDescriptor 陷阱的返回值必须是
null、undefined或一个对象;
如果返回对象,则对象自己的属性只能是
enumerable、configurable、vlaue、writable、get和set;
在返回的对象中使用不被允许的属性会抛出一个错误。
let proxy = new Proxy({}, {
getOwnPropertyDescriptor(trapTarget, key) {
return {
name: "proxy"
};
}
});
// 给不存在的属性赋值会抛出错误
// Uncaught TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for property 'name' which is either non-existant or configurable in the proxy target
let descriptor = Object.getOwnPropertyDescriptor(proxy, "name");
这条限制可以确保无论代理中使用了什么方法,Object.getOwnPropertyDescriptor() 返回值的结构总是可靠的。
JS 的这种行为在非常大型的项目中,可能会导致严重的问题,尤其是当属性名称存在书写错误时。我们可以使用代理对访问不存在的属性时,抛出错误。
对象可扩展性陷阱
ECMAScript 5 已经通过 Object.preventExtensions() 方法和
Object.isExtensible() 方法修正了对象的可扩展性;
ECMAScript 6 可以通过代理中的 preventExtensions 和 isExtensible
陷阱拦截这两个方法并调用底层对象。
两个陷阱都接受唯一参数 trapTarget 对象,并调用它上面的方法。
isExtensible 陷阱返回的一定是一个 boolean 值,表示对象是否可扩展;
preventExtensions 陷阱返回的也一定是布尔值,表示操作是否成功。
Reflect.preventExtensions() 方法和 Reflect.isExtensible() 方法实现了相应陷阱中的默认行为,二两都返回布尔值。
...const r = { _isRef: true, get value() { track(r, "get" /* GET */, 'value'); return raw; }, set value(newVal) { raw = convert(newVal); // trigger 方法扮演通信员的角色,贯穿整个响应系统,使得 ref 具有高度的响应性 trigger(r, "set" /* SET */, 'value', { newValue: newVal } ); }};return r...
自定义数组类型
function toUint32(value) {
return Math.floor(Math.abs(Number(value))) % Math.pow(2, 32);
}
function isArrayIndex(key) {
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2, 32) - 1);
}
class MyArray {
constructor(length = 0) {
this.length = length;
return new Proxy(this, {
set(trapTarget, key, value) {
let currentLength = Reflect.get(trapTarget, "length");
if (isArrayIndex(key)) {
let numericKey = Number(key);
if (numericKey >= currentLength) {
Reflect.set(trapTarget, "length", numericKey + 1);
}
} else if (key === "length") {
if (value < currentLength) {
for (let index = currentLength - 1; index >= value; index--) {
Reflect.deleteProperty(trapTarget, index);
}
}
}
return Reflect.set(trapTarget, key, value);
}
});
}
}
let colors = new MyArray(3);
console.log(colors instanceof MyArray); // true
console.log(colors.length); // 3
let colors2 = new MyArray(5);
console.log(colors.length); // 3
console.log(colors2.length); // 5
colors[0] = "red";
colors[1] = "green";
colors[2] = "blue";
colors[3] = "black";
console.log(colors.length); // 4
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
console.log(colors[0]); // "red"
使用代理的好处是,对目标对象target架设了一层拦截,可以对外界的访问进行过滤和改写,不用再递归遍历对象的所有属性并进行getter/setter转换操作,这使得组件更快的初始化,运行时的性能上将得到极大的改进,据测试新版本的 Vue 比之前 速度快了2倍(非常夸张)。
数组问题
在ECMAScript6出现之前,开发者不能通过自己定义的对象模仿JavaScript数组对象的行为方式。当给数组的特定元素赋值时,影响到该数组的length属性,也可以通过length属性修改数组元素。
let colors = ["red", "green", "blue"];
console.log(colors.length); // 3
colors[3] = "black";
console.log(colors.length); // 4
console.log(colors[3]); // "black"
colors.length = 2;
console.log(colors.length); // 2
console.log(colors[3]); // undefined
console.log(colors[2]); // undefined
console.log(colors[1]); // "green"
Note
数值属性和length属性具有这种非标准行为,因而在ECMAScript中数组被认为是奇异对象(exotic object,与普通对象相对)。
let target = {name: "target"};let handler = { set(target, key, value, receiver) { // 拦截,忽略已有属性,避免影响它们 if (!target.hasOwnProperty(key)) { if (isNaN(value)) { throw new TypeError("Property must be a number."); } } // 满足条件的进行写入 等价于 target[key] = value; return Reflect.set(target, key, value, receiver); }}let proxy = new Proxy(target, handler);// 添加一个新属性proxy.count = 1;console.log(proxy.count); // 1console.log(target.count); // 1// 你可以为 name 赋一个非数值类型的值,因为该属性已经存在proxy.name = "proxy";console.log(proxy.name); // "proxy"console.log(target.name); // "proxy"// 抛出错误proxy.anotherName = "proxy";
描述符对象限制
陷阱函数 ownKeys
原型代理陷阱的运行机制
原型代理陷阱有一些限制:
getPrototypeOf陷阱必须返回对象或null
在setPrototypeOf陷阱中,如果操作失败则返回的一定是false,此时Object.setPrototypeOf()会抛出错误,如果setPrototypeOf返回了任何不是false的值,那么Object.setPrototypeOf()便假设操作成功。
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, {});
// 给不存在的属性赋值会抛出错误: // Uncaught TypeError: 'setPrototypeOf' on proxy: trap returned falsish Object.setPrototypeOf(proxy, {});
可以使用Reflect上的对应方法实现这两个陷阱的默认行为。
let target = {};
let proxy = new Proxy(target, {
getPrototypeOf(trapTarget) {
return Reflect.getPrototypeOf(trapTarget);
},
setPrototypeOf(trapTarget, proto) {
return Reflect.setPrototypeOf(trapTarget, proto);
}
});
let targetProto = Object.getPrototypeOf(target);
let proxyProto = Object.getPrototypeOf(proxy);
console.log(targetProto === Object.prototype); // true
console.log(proxyProto === Object.prototype); // true
// 成功
Object.setPrototypeOf(target, {});
// 成功:
Object.setPrototypeOf(proxy, {});
set()陷阱函数,对目标对象上不存在的属性设置值时,进行 “添加” 操作,并且会触发trigger()来通知响应系统的更新。解决了 Vue 2.x 中无法检测到对象属性的添加的问题。
深入源码
ownKeys陷阱函数也能影响for-in循环,因为这种循环调用了陷阱函数来决定哪些值能够被用在循环内。(Vue 源码会涉及这里)
ownKeys()陷阱函数接受单个参数,即目标对象,同时必须返回一个数组或者一个类数组对象,不合要求的返回值会导致错误。
get()会自动读取使用ref对象创建的响应数据,并进行track调用。
这种变更检测机制存在一个限制,那就是Vue 无法检测到对象属性的添加或删除。为此我们需要使用Vue.set和Vue.delete来保证响应系统的运行符合预期。
下文介绍到的陷阱函数,都会在 Vue 3 源码中出现,提前进行了解。
陷阱函数
创建响应式数据
let target = { name: "target", value: 42}let proxy = new Proxy(target, { deleteProperty(target, key) { // 拦截行为 if (key === "value") { return false; } else { // 恢复行为 return Reflect.deleteProperty(target, key); } }})console.log("value" in proxy); // true// 尝试删除 proxy.valuedelete proxy.value; // false // 不能删除,因为这个默认行为被拦截了console.log("value" in proxy); // trueconsole.log("name" in proxy); // true// 尝试删除 proxy.namedelete proxy.name; // trueconsole.log("name" in proxy); // false
到这里不明白没关系,在下文会介绍的陷阱函数中,应该就会明白了。
时间: 2019-11-15阅读: 98标签: vue3新版本前瞻
value属性是不能被删除的,因为该操作被proxy对象拦截。这么做允许你在严格模式下保护属性避免其被删除,并且不会抛出错误。
let target = {};let proxy = new Proxy(target, { get: function(target, property) { return 35; }});proxy.time // 35proxy.name // 35proxy.title // 35
我们已经知道,通过调用new Proxy()可以创建一个代理用来替代目标对象target。这个代理对目标对象进行了虚拟,因此该代理与该目标对象表面上可以被当作同一个对象来对待。
ref对象有一个指向内部值的单个属性.value。如果将一个值分配为ref对象,则reactive()方法会使该对象具有高度的响应性。
结合目前的RFCs和已经完成的改进,可以窥探到 Vue 3.0 将带来:
使用get()陷阱函数与Reflect.get()方法在目标属性不存在时抛出错误:
全新的变更检测
背景
has() 和 ownKeys()
let target = {};console.log(target.name); // undefined
function set(target, key, value, receiver) { value = toRaw(value); // 获取修改之前的值,进行一些处理 const oldValue = target[key]; if (isRef(oldValue) !isRef(value)) { oldValue.value = value; return true; } const hadKey = hasOwn(target, key); // 恢复默认行为 const result = Reflect.set(target, key, value, receiver); // //如果目标对象在原型链上,不要 trigger if (target === toRaw(receiver)) { /* istanbul ignore else */ { const extraInfo = { oldValue, newValue: value }; // 如果设置的属性不在目标对象上 就进行 Add // 这就解决了 Vue 2.x 中无法检测到对象属性的添加或删除的问题 if (!hadKey) { trigger(target, "add" /* ADD */ , key, extraInfo); } else if (hasChanged(value, oldValue)) { // trigger 方法进行一系列的调度工作,贯穿着整个响应系统,是变更检测的“通讯员” trigger(target, "set" /* SET */ , key, extraInfo); } } } return result;}
// 根组件template div div{{ name }}/div /div/templatescriptimport { createApp } from Vue;export default {const App = { data: { name: 'Vue 3', // count: ref(0) }}createApp().mount(App, '#app')/script
reactive()函数最终返回一个可观察的响应式Proxy代理。
简单的来讲,Proxy 是拦截默认行为,Reflect 是恢复默认行。被 Proxy 拦截、过滤了一些默认行为之后,可以使用 Reflect 恢复未被拦截的默认行为。通常它们两个会结合在一起使用。
name属性是不可配置的,因此对其使用delete操作符只会返回false(如果代码运行在严格模式下,则会抛出错误)。可以在代理对象中使用deleteProperty()陷阱函数以改变这种行为。
Reflect.deleteProperty()方法也接受这两个参数,并提供了deleteProperty()陷阱函数的默认实现。
模块化架构,支持 tree-shaking。API 暴露为函数。Composition API + Options API。基于 Proxy 的变更检测。支持 Fragments。支持 Portals。支持 Suspense w/ async setup()。全局挂载/配置 API 更改(createApp().mount(...))。Component v-model API 更改。Custom Directive API 更改。函数组件和异步组件 API 更改。Render 函数 API 更改。...
...function proxy (target, sourceKey, key) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] }; sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val; }; Object.defineProperty(target, key, sharedPropertyDefinition);}...for (const key in propsOptions) { ... if (!(key in vm)) { proxy(vm, `_props`, key); }}
获取一个内部值并返回一个响应式的可变ref对象。
如果未提供陷阱函数,代理会对所有操作采取默认行为。
陷阱函数 deleteProperty
target:需要读取属性的对象(即代理的目标对象);key:需要检查的属性的键(字符串类型或符号类型)。
本文由10bet发布于Web前端,转载请注明出处:【读书笔记】【深入理解ES6】#12-代理(Proxy)和
关键词: