# Vue3 核心技术 写这篇文章是之前看到了尤大的微博,Vue3 发布了新版本,其在声明中写到对于之前的 Object.defineProperty 用 Proxy 改写了。 > The Proxy object is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) ## 为什么要改写? Vue3 之前的版本响应式是通过 `Object.defineProperty` 实现的。getter 用来依赖收集,setter 在数据变化时通知订阅者更新视图。这种方式存在一些缺陷: 1. 无法检测到对象属性的新增或者删除。 Object.defineProperty 只会是属性的读写才会触发,所以新增、删除都无法检测。Vue 为了做到新增、删除也是响应式,做了一些手段 `Vue.set(obj, propertName/index, value)`、响应式对象的子对象新增属性,可以给子响应式对象重新赋值。 2. 不能监听数组的变化 Vue 在数组做了 hack,把无法监听的情况通过重写数组某些方法实现响应式。数组的操作也限制在 `push/pop/shift/unshift/splice/sort/reverse` 7个方法。 ## Proxy 如何解决? Vue3 利用 Proxy 实现数据读取和设置拦截,在拦截 trap 中实现数据依赖收集和触发视图更新的操作。 ```Javascript function get(target, key, receiver) { // handler.get的拦截实现 const res = Reflect.get(target, key, receiver) if(isSymbol(key) && builtInSymbols.has(key)) return res if (isRef(res)) return res.value track(target, OperationTypes.GET, key) // 收集依赖 return isObject(res) ? reactive(res) : res } // handler.set的拦截操作 function set(target, key, value, receiver) { value = toRaw(value) // 获取缓存响应数据 oldValue = target[key] if (isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } const result = Reflect.set(target, key, value, receiver) if (target === toRaw(receiver)) { //set拦截只限对象本身 ... // 不同环境操作处理,并省略下面trigger方法第二参数获取逻辑 trigger(target, OperationTypes.x, key) // 触发视图更新 } return result } ``` ## Proxy ? Proxy - “代理”。当访问对象之前增加一个中间层,这样就不直接访问对象,而是通过中间层做一个中转。这个中间层就是本文主角 - **Proxy**。相信大家很早听到代理可能是`网络代理`。比如你需要访问国外站点,请求的时候你的电脑发起的网络请求被代理服务器做转发,然后再请求真正的目的服务器,响应的时候国外站点将响应发送到代理服务器,代理服务器再将数据内容返回到你的电脑。 `网络代理`分为「正向代理」和「反向代理」。nginx 就是架设在用户浏览器和目的服务器之间的中间层。 浏览器通过 nginx 才可以访问到服务器。通过代理可以实现负载均衡、访问控制等目的。 Javascript 中的 Proxy 类似。区别在于 Proxy 是对象和对象之间的一层代理。应用了 Proxy 之后,我们通过 Proxy 来访问或者操作目标对象。Proxy 会改变 Javascript 中的一些基础操作执行路径,比如属性读写、对象遍历、函数调用等。这种强大的能力被叫做**元编程**(meta programming)机制,可实现很多之前无法实现的功能。 Proxy 会修改 Javascript 的一些底层代码执行方式,所以它无法被完全 polyfill。 ## 核心概念 - target:指的是目标对象,也就是被代理的对象 - trap:可以理解为一些预定义的触发点以及定义在这些触发点上的函数。比如属性的读取、函数调用等。如果在这些触发点上定义了函数,那么在对 Proxy 执行对应操作的时候,定义的函数将会被调用 - handler:是一个对象,包装了所有 Proxy 提供的 trap 函数,可以认为是 trap 的集合 ## Proxy 创建 在以下简单的例子中,当对象中不存在属性名时,缺省返回数为37。例子中使用了 get。 ```Javascript let handler = { get: function(target, name){ return name in target ? target[name] : 37; } }; let proxy = new Proxy({}, handler); proxy.a = 1; proxy.b = undefined; console.log(proxy.a, proxy.b); // 1, undefined console.log('c' in proxy, proxy.c); // false, 37 ``` 上面的代码定义了 Proxy 对象 proxy,被代理的对象是一个空对象 {}, 有了代理对象,对被代理对象的操作都会通过代理对象进行处理。当访问 proxy.c 的时候,我们定义的 handle 对象中的 get 函数(trap)会被调用。 get 函数接受的参数分别为 target对象和需要访问的属性名称。 ## 有哪些 traps? 所有的traps是可选的。如果某个trap没有定义,那么默认的行为会应用到目标对象上。 - handler.getPrototypeOf() 在读取代理对象的原型时触发该操作,比如在执行 Object.getPrototypeOf(proxy) 时。 - handler.setPrototypeOf() 在设置代理对象的原型时触发该操作,比如在执行 Object.setPrototypeOf(proxy, null) 时。 - handler.isExtensible() 在判断一个代理对象是否是可扩展时触发该操作,比如在执行 Object.isExtensible(proxy) 时。 - handler.preventExtensions() 在让一个代理对象不可扩展时触发该操作,比如在执行 Object.preventExtensions(proxy) 时。 - handler.getOwnPropertyDescriptor() 在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时。 - handler.defineProperty() 在定义代理对象某个属性时的属性描述时触发该操作,比如在执行 Object.defineProperty(proxy, "foo", {}) 时。 - handler.has() 在判断代理对象是否拥有某个属性时触发该操作,比如在执行 "foo" in proxy 时。 - handler.get() 在读取代理对象的某个属性时触发该操作,比如在执行 proxy.foo 时。 - handler.set() 在给代理对象的某个属性赋值时触发该操作,比如在执行 proxy.foo = 1 时。 - handler.deleteProperty() 在删除代理对象的某个属性时触发该操作,即使用delete运算符,比如在执行 delete proxy.foo 时。 - handler.ownKeys() Object.getOwnPropertyNames 和Object.getOwnPropertySymbols的trap. - handler.apply() 当目标对象为函数,且被调用时触发。 - handler.construct() new 运算符的trap。 ## Reflect **Reflect** 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与处理器对象的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。 与大多数全局对象不同,Reflect 不是一个构造函数。你不能将其与一个 new 运算符一起使用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是**静态**的(就像Math对象) ```Javascript let handler = { get: function(target, name){ return Reflect.get(target, name); } }; let proxy = new Proxy({}, handler); proxy.a = 1; proxy.b = undefined; console.log(proxy.a, proxy.b); // 1, undefined console.log('c' in proxy, proxy.c); // false, 37 ``` Reflect 对象提供和了以下 static 方法,用来方便写 trap 函数时将操作传递到目标对象上。 - Reflect.apply() 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。 - Reflect.construct() 对构造函数进行 new 操作,相当于执行 new target(...args)。 - Reflect.defineProperty() 和 Object.defineProperty() 类似。 - Reflect.deleteProperty() 作为函数的delete操作符,相当于执行 delete target[name]。 - Reflect.enumerate() 该方法会返回一个包含有目标对象身上所有可枚举的自身字符串属性以及继承字符串属性的迭代器,for...in 操作遍历到的正是这些属性。 - Reflect.get() 获取对象身上某个属性的值,类似于 target[name]。 - Reflect.getOwnPropertyDescriptor() 类似于 Object.getOwnPropertyDescriptor()。 - Reflect.getPrototypeOf() 类似于 Object.getPrototypeOf()。 - Reflect.has() 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。 - Reflect.isExtensible() 类似于 Object.isExtensible(). - Reflect.ownKeys() 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响). - Reflect.preventExtensions() 类似于 Object.preventExtensions()。返回一个Boolean。 - Reflect.set() 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。 - Reflect.setPrototypeOf() 类似于 Object.setPrototypeOf()。 ## Proxy 优缺点 优点: 看上去的 getter、setter 的功能一样,都可以做到当访问或者改写一个对象的时候调用某个特定的函数。但是还是有区别的: 1. Proxy除了对属性值的获取和改写还可以改变其他 JavaScript 基础操作的执行方式,也就是它的功能比 getter、setter 多很多 2. getter、setter 是定义在对象本身上的,而 Proxy 是定义在另一个对象上的。这表示 **Proxy 允许我们比较方便的修改一些我们不方便直接改变其定义方式的对象的执行方式,比如一些第三方库中定义的对象等**。 缺点: 1. 兼容性问题,无完全 polyfill 虽然大部分浏览器支持 Proxy 特性,但是一些浏览器或者其低版本不支持 Proxy,其中 IE、QQ 浏览器、百度浏览器等完全不支持,因此 Proxy 有兼容性问题。那能否像ES6其他特性那样有对应的 polyfill 解决方案呢,答案并不那么乐观。其中作为ES6转换的翘楚 Babel,在其官网明确做了说明: > Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled. 也就是说,由于ES5的限制,ES6的Proxy没办法被完全polyfill,所以babel没有提供对应的转换支持,Proxy的实现是需要JS引擎级别提供支持,目前大部分主要的JS引擎提供了支持 2. 性能问题 Proxy 的性能比 Promise 还差,这就要需要在性能和简单实用上进行权衡。另外,Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,性能这块相信会逐步得到改善。