ES6+ Proxy
本节我们将学习 ES6 的新增知识点 ——Proxy,Proxy 是代理的意思。Proxy 是一个对象,用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。这是 MDN 上的定义,但是不容易理解,想要理解 Proxy 我们首先需要知道什么是代理?
在日常开发中比较常见的代理常见有,使用 Charles 代理抓包、nginx 服务器的反向代理,以及 VPN 等,都用到了代理。什么是代理呢?我们先看一张图:
上图是客户端访问网络的示意图,客户端不能直接访问网络,它只能先访问代理服务器,只有代理服务器才能有权限访问,然后代理服务器把客户端请求的信息转发给目标服务器,最后代理服务器在接收到目标服务器返回的结果再转发给客户端,这样就完成了整个请求的响应过程。这是现在大多数服务器的架构,我们可以把上图的 Proxy Server 理解为 Nginx。代理有正向代理和反向代理,有兴趣的小伙伴可以去深入了解一下。
本节说的 Proxy 就是作用在 JavaScript 中的一种代理服务,代理的过程其实就是一种对数据的劫持过程,Proxy 可以对我们定义的对象的属性进行劫持,当我们访问或设置属性时,会去调用对应的钩子执行。在 ES5 中我们曾学习过 Object.defineProperty()
它的作用和 Proxy 是相同的,但是 Object.defineProperty()
存在一些性能问题,Proxy 对其进行了升级和扩展更加方便和易用。本节我们将学习 Proxy 的使用。
在学习 Proxy 之前,我们先来回归一下 ES5 中的 Object.defineProperty()
,接触过前端框架的同学应该都知道 Vue 和 React,其中 Vue 中的响应式数据底层就是使用 Object.defineProperty()
这个 API 来实现的。下面是 Object.defineProperty()
的语法。
Object.defineProperty(obj, prop, descriptor)
代码块预览 复制
Object.defineProperty()
会接收三个参数:
- obj 需要观察的对象;
- prop 是 obj 上的属性名;
- descriptor 对 prop 属性的描述。
当我们去观察一个对象时需要在 descriptor 中去定义属性的描述参数。在 descriptor 对象中提供了 get 和 set 方法,当我们访问或设置属性值时会触发对应的函数。
var obj = {}; var value = undefined; Object.defineProperty(obj, "a", { get: function() { console.log('value:', value) return value; }, set: function(newValue) { console.log('newValue:', newValue) value = newValue; }, enumerable: true, configurable: true }); obj.a; // value: undefined obj.a = 20; // newValue: 20
代码块预览 复制
上面的代码中,我们使用一个变量 value 来保存值,这里需要注意的是,不能直接使用 obj 上的值,否则就会出现死循环。
Object.defineProperty()
是 Vue2 的核心, Vue2 在初始化时会对数据进行劫持,如果劫持的属性还是对象的话需要递归劫持。下面我们把 Vue2 中数据劫持的核心代码写出来。
var data = { name: 'imooc', lession: 'ES6 Wiki', obj: { a: 1 } } observer(data); function observer(data) { if (typeof data !== 'object' || data == null) { return; } const keys = Object.keys(data); for (let i = 0; i < keys.length; i++) { let key = keys[i]; let value = obj[key]; defineReactive(obj, key, value); } } function defineReactive(obj, key, value) { observer(value); Object.defineProperty(obj, key, { get() { return value; }, set(newValue) { if (newValue === value) return; observer(newValue); value = newValue; } }) }
代码块预览 复制
上面代码的核心是 defineReactive 方法,它是递归的核心函数,用于重新定义对象的读写。从上面的代码中我们发现 Object.defineProperty()
是有缺陷的,当观察的数据嵌套非常深时,这样是非常耗费性能的,这也是为什么现在 Vue 的作者极力推广 Vue3 的原因之一,Vue3 的底层使用了 Proxy 来代替 Object.defineProperty()
那 Proxy 具体有什么好处呢?
首先我们来看下 Proxy 是如何使用的,语法:
const p = new Proxy(target, handler)
代码块预览 复制
Proxy 对象是一个类,需要通过 new 去实例化一个 Proxy 对象,它接收的参数比较简单,只有两个:
- target:需要使用 Proxy 进行观察的目标对象;
- handler:对目标对象属性进行处理的对象,包含了处理属性的回调函数等。
const handler = { get: function(obj, prop) { return obj[prop]; }, set: function(obj, prop, value) { return obj[prop] = value; } }; const p = new Proxy({}, handler); p.a = 1; console.log(p.a, p.b); // 1, undefined
代码块预览 复制
对比上面的 Object.defineProperty()
API 直观的看 Proxy 做了一些精简,把对象、属性和值作为 get 和 set 的参数传入进去,不必考虑死循环的问题了。这是直观的感受。
上面我们使用了 Object.defineProperty()
API 简单地实现了 Vue2 的响应式原理,那么 Vue 使用 Proxy 是怎么实现的呢?它带来了哪些好处呢?下面我们看实现源码:
var target = { name: 'imooc', lession: 'ES6 Wiki', obj: { a: 1 } } var p = reactive(target); console.log(p.name); // 获取值: imooc p.obj.a = 10; // 获取值: {a : 1} console.log(p.obj.a); // 获取值: {a : 10} function reactive(target) { return createReactiveObject(target) } function createReactiveObject(target) { // 判断如果不是一个对象的话返回 if (!isObject(target)) return target // target观察前的原对象; proxy观察后的对象:observed observed = new Proxy(target, { get(target, key, receiver) { const res = target[key]; console.log('获取值:', res) // todo: 收集依赖... return isObject(res) ? reactive(res) : res }, set(target, key, value, receiver) { target[key] = value; } }) return observed }
代码块预览 复制
上面的代码是从 Vue3 中摘出来的 reactive 函数的实现,我们可以直观地看到没有对 target 进行递归循环去创建观察对象。而且,当我们对 obj 下的 a 属性设置值时,执行 get 函数,这是为什么呢?这就是 Proxy 的优点,在对 obj 下属性设置值时,首先需要调用 set 方法获取 target 下 obj 的值,然后判断 obj 又是一个对象再去调用 reactive 函数进行观察。这样就不需要递归地去对嵌套数据进行观察了,而是在获取值的时候,判断获取的值是不是一个对象,这样极大地节约了资源。
本节主要通过代理和 Object.defineProperty()
API 的学习来理解 ES6 的新增知识点 ——Proxy,并且通过 Vue2 和 Vue3 实现响应式原理来对比 Object.defineProperty()
和 Proxy 的优缺点,从而更深入地理解 Proxy。