Proxy和响应式原理
Proxy
-
概念和语法
-
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等) -
Proxy的语法格式如下/** * target表示的就是要拦截(代理)的目标对象;而handler是用来定制拦截行为 * 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值 */ const proxy = new Proxy(target, handler)可以将
Proxy理解成“拦截”,在目标对象之前架设一层“拦截”,当外界对该对象的访问,都必须先通过这层拦截,正因为有了一种拦截机制,当外界的访问我们可以对进行一些操作(过滤或改写)
-
-
handler中的方法
-
get- get方法可自动接受3个参数target, propKey, receiver,分别表示要代理的目标对象、对象上的属性以及代理对象,该方法用于拦截某个属性的读取操作,比如
proxy.foo和proxy['foo']。
- get方法可自动接受3个参数target, propKey, receiver,分别表示要代理的目标对象、对象上的属性以及代理对象,该方法用于拦截某个属性的读取操作,比如
-
set- set方法可自动接受4个参数:target, propKey, value, receiver,分别表示要代理的目标对象、对象上的属性、属性对应的值以及代理对象。该方法用于拦截对象属性操作,像
proxy.foo = xxx或proxy['foo'] = xxx。
- set方法可自动接受4个参数:target, propKey, value, receiver,分别表示要代理的目标对象、对象上的属性、属性对应的值以及代理对象。该方法用于拦截对象属性操作,像
-
has- has方法接受target, propKey,用于拦截
propKey in proxy的操作,返回一个布尔值,表示属性是否存在。
- has方法接受target, propKey,用于拦截
-
deleteProperty- 可接收target, propKey,用于拦截delete操作,返回一个布尔值,表示是否删除成功。
-
ownKeys- 可接收target,用于拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环等类似操作,返回一个数组,表示对象所拥有的keys。
- 可接收target,用于拦截
-
getOwnPropertyDescriptor- 接收target和propKey,用于拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
- 接收target和propKey,用于拦截
-
defineProperty- 接收target, propKey, propDesc,分别表示目标对象、目标对象的属性,以及属性描述配置,用于拦截
Object.defineProperty(proxy, propKey, propDesc)和Object.defineProperties(proxy, propDescs)的操作。
- 接收target, propKey, propDesc,分别表示目标对象、目标对象的属性,以及属性描述配置,用于拦截
-
preventExtensions- 可接收target,用于拦截
Object.preventExtensions(proxy)操作,补充说明一下preventExtensions的作用是将一个对象变成不可扩展,也就是永远不能再添加新的属性。
- 可接收target,用于拦截
-
getPrototypeOf(target)- 在使用
Object.getPrototypeOf(proxy)会触发调用,返回一个对象。
- 在使用
-
isExtensible(target)- 当使用
Object.isExtensible(proxy)时会触发调用,返回一个布尔值,表示是否可扩展。
- 当使用
-
setPrototypeOf(target, proto)- 当调用
Object.setPrototypeOf(proxy, proto)会触发该函数调用。
- 当调用
-
apply(target, object, args)- 接收三个参数target, object, args,分别表示目标对象、调用函数是的this指向以及参数列表,当
Proxy实例作为函数调用时触发,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
- 接收三个参数target, object, args,分别表示目标对象、调用函数是的this指向以及参数列表,当
-
construct(target, args)- 接收target和args,表示目标函数即参数列表,当
Proxy实例作为构造函数时触发该函数调用,比如new proxy(...args)。
- 接收target和args,表示目标函数即参数列表,当
-
-
总结
- 代理对象不等于目标对象,他是目标对象的包装品
- 目标对象既可以直接操作,也可以被代理对象操作,且两者相互关联
- 如果直接操作目标对象,则会绕过代理定义的各种拦截行为
- 如果用了代理,那肯定是希望给对象的操作嵌入我们定义的特殊行为,所以一般就操作代理对象就好
Reflect
-
概述
Reflect对象与Proxy对象一样,也是ES6为了操作对象而提供的新的API。Reflect对象设计的目的主要有以下几个:
-
将 Object 对象的一些明显属于语言内部的方法(比如
Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以获得语言内部的方法。 -
修改某些 Object 方法的返回结果,让其变得更合理。比如,
Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回 false 。// 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure } -
让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如
name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true
-
Reflect对象的方法与Proxy对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,我们总可以在 Reflect 上获取默认行为。Proxy(target, { set: function(target, name, value, receiver) { var success = Reflect.set(target, name, value, receiver); if (success) { console.log('property ' + name + ' on ' + target + ' set to ' + value); } return success; } });上面代码中, Proxy 方法拦截 target 对象的属性赋值行为。它采用 Reflect.set 方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。
下面是另一个例子。
var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); } });上面代码中,每一个 Proxy 对象的拦截操作( get 、 delete 、 has ),内部都调用对应的 Reflect 方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。
有了 Reflect 对象以后,很多操作会更易读。
// 老写法 Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 新写法 Reflect.apply(Math.floor, undefined, [1.75]) // 1
-
-
静态方法
Reflect对象一共有 13 个静态方法(匹配Proxy的13种拦截行为)。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)- 用途
先看一个复杂的例子。
let user = { _name: "张三", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { return Reflect.get(target, prop); // return target[prop]; // (*) target = user } }); let admin = { __proto__: userProxy, _name: "李四" }; // except 李四 console.log(admin.name); // 张三上述的情况可以看到无论是使用
Reflect.get(target, prop)还是target[prop], 都是张三。如何在这种情况下,正确的传递上下文,是个问题。如果是普通的函数的话,我们还可以通过call/apply,但在这里我们是getter,而不是调用。get(target, prop, receiver)有第三个参数没有使用,而且我们也知道 Reflect 的方法的参数是和 Proxy handler 一致的,那我们试试将receiver传递进入Reflect.get,看修改以后的效果。let user = { _name: "张三", get name() { return this._name; } }; let userProxy = new Proxy(user, { get(target, prop, receiver) { // receiver = admin return Reflect.get(target, prop, receiver); } }); let admin = { __proto__: userProxy, _name: "李四" }; console.log(admin.name); // => 李四可以看到第三个参数
receiver保持了正确的this引用,在示例中,指向了admin。在复杂的使用场景保持正确的上下文,这是 Reflect 一系列 API的一个重要意义所在。基于发布—订阅模式的响应式原理
-
发布—订阅模式
-
如下图,可以看到有三个主要元素
-
发布者
-
订阅者
-
消息中心
-
-
- 发布和订阅都是跟消息中心通信,从而达到解耦。
- vue3中的响应式:`reactive`包装数据,`effect`定义数据变化后的回调。
-
基于
Object.defineProperty的响应式-
Object.defineProperty的作用- 劫持一个对象的属性,通常我们对属性的
getter和setter方法进行劫持,在对象的属性发生变化时进行特定的操作。
- 劫持一个对象的属性,通常我们对属性的
-
Object.defineProperty的缺陷- 无法监听数组变化。
- 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。
-
Proxy在ES2015规范中被正式发布,可以这样认为,Proxy是
Object.defineProperty的全方位加强版。
-
-
基于Proxy实现响应式
-
reactive():为目标对象创建一个Proxy对象(代理对象)。function reactive(target) { return new Proxy(target, { get(target, key, receiver) { //订阅 track(target, key); return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // 发布 trigger(target, key); return result; }, }); } -
effect():注册副作用函数机制// 用一个全局变量储存被注册的副作用函数 let activeEffect; // 用于注册副作用函数 function effect(fn) { const effectFn = () => { activeEffect = effectFn; fn(); }; // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合 effectFn.deps = []; effectFn(); fn(); } -
track():订阅函数// 订阅:将依赖存入bucket(消息中心) const bucket = new WeakMap(); function track(target, key) { if (!activeEffect) return; // 根据target从bucket中取的desMap,没有则新建一个与target关联 let depsMap = bucket.get(target); if (!depsMap) { bucket.set(target, (depsMap = new Map())); } // 再根据key从depsMap中取得deps,它是一个Set类型,里面存储着所有与当前key相关连的依赖 let deps = depsMap.get(key); if (!deps) { depsMap.set(key, (deps = new Set())); } // 把当前激活的副作用函数添加到依赖集合deps中 deps.add(activeEffect); // deps就是一个与当前副作用函数存在联系的依赖集合,将其添加到activeEffect.deps中 activeEffect.deps.push(deps); } -
trigger():发布函数// 发布:当侦听到对应数据变化并且在bucket中能找到相应的回调函数时,执行即可 function trigger(target, key) { const depsMap = bucket.get(target); if (!depsMap) return; const effects = depsMap.get(key); effects && effects.forEach(effectFn => effectFn()) }
-