【读书笔记】【深入理解ES6】#12-代理(Proxy)和

来源:http://www.chinese-glasses.com 作者:Web前端 人气:197 发布时间:2020-03-16
摘要:value是对象自身的属性,而toString则是原型属性,可以使用代理的has()陷阱函数来拦截这个操作,从而在使用in运算符时返回不同的结果。 将代理用作类的原型 function NoSuchProperty() {}let

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

原型代理陷阱的运行机制

原型代理陷阱有一些限制:

  1. getPrototypeOf陷阱必须返回对象或null

  2. 在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)和

关键词:

上一篇:Ember.js和Vue.js对比,哪个框架更优秀?

下一篇:没有了

最火资讯