vue3.0 的 pre-alpha https://github.com/vuejs/vue-next 版代码已经开源了, 就像作者之前放出的消息一样, 其数据响应这一部分已经由 ES6 的 Proxy 来代替 Object.defineProperty 实现, 感兴趣的同学可以看其实现源码. vue 都开始使用 Proxy 来实现数据的响应式了, 所以有必要抽点时间了解下 Proxy.
Object.defineProperty 的缺陷
说到 Proxy, 就不得不提 Object.defineProperty, 我们都知道, vue3.0 之前的版本都是使用该方法来实现数据的响应式, 具体是:
通过设定对象属性 getter/setter 方法来监听数据的变化, 同时 getter 也用于依赖收集, 而 setter 在数据变更时通知订阅者更新视图.
大概如下代码所示:
- function defineReactive(obj, key, value) {
- Object.defineProperty(obj, key, {
- enumerable: true,
- configurable: true,
- get() {
- collectDeps() // 收集依赖
- return value
- },
- set(newVal) {
- observe(newVal); // 若是对象需要递归子属性
- if (newVal !== value) {
- notifyRender() // 通知订阅者更新
- value = newVal;
- }
- }
- })
- }
- function observe(obj) {
- if (!obj || typeof obj! === 'object') {
- return
- }
- Object.keys(obj).forEach(key => {
- defineReactive(obj, key, obj[key]);
- })
- }
- var data = {
- name: 'wonyun',
- sex: 'male'
- }
- observe(data)
虽然 Object.defineProperty 通过为属性设置 getter/setter 能够完成数据的响应式, 但是它并不算是实现数据的响应式的完美方案, 某些情况下需要对其进行修补或者 hack, 这也是它的缺陷, 主要表现在两个方面:
无法检测到对象属性的新增或删除
由于 JS 的动态性, 可以为对象追加新的属性或者删除其中某个属性, 这点对经过 Object.defineProperty 方法建立的响应式对象来说, 只能追踪对象已有数据是否被修改, 无法追踪新增属性和删除属性, 这就需要另外处理.
目前 Vue 保证响应式对象新增属性也是响应式的, 有两种方式:
Vue.set(obj, propertName/index, value)
响应式对象的子对象新增属性, 可以给子响应式对象重新赋值
- data.location = {
- x: 100,
- y: 100
- }
- data.location = {...data, z: 100}
响应式对象删除属性, 可以使用 Vue.delete(obj, propertyName/index) 或者 vue.$delete(obj, propertyName/index); 类似于删除响应式对象子对象的某个属性, 也可以重新给子对象赋值来解决.
不能监听数组的变化
vue 在实现数组的响应式时, 它使用了一些 hack, 把无法监听数组的情况通过重写数组的部分方法来实现响应式, 这也只限制在数组的 push/pop/shift/unshift/splice/sort/reverse 七个方法, 其他数组方法及数组的使用则无法检测到, 例如如下两种使用方式:
- vm.items[index] = newValue
- vm.items.length--
那么 vue 怎么实现数组的响应式呢, 并不是重写数组的 Array.prototype 对应的方法, 具体来说就是重新指定要操作数组的 prototype, 并重新该 prototype 中对应上面的 7 个数组方法, 通过下面代码简单了解下实现原理:
- const methods = ['pop','shift','unshift','sort','reverse','splice', 'push'];
- // 复制 Array.prototype, 并将其 prototype 指向 Array.prototype
- let proto = Object.create(Array.prototype);
- methods.forEach(method => {
- proto[method] = function () { // 重写 proto 中的数组方法
- Array.prototype[method].call(this, ...arguments);
- viewRender() // 视图更新
- }
- })
- function observe(obj) {
- if (Array.isArray(obj)) { // 数组实现响应式
- obj.__proto__ = proto; // 改变传入数组的 prototype
- return;
- }
- if (typeof obj === 'object') {
- ... // 对象的响应式实现
- }
- }
Proxy 的使用
Proxy, 字面意思是代理, 是 ES6 提供的一个新的 API, 用于修改某些操作的默认行为, 可以理解为在目标对象之前做一层拦截, 外部所有的访问都必须通过这层拦截, 通过这层拦截可以做很多事情, 比如对数据进行过滤, 修改或者收集信息之类. 借用 proxy 的巧用 https://juejin.im/post/5d2e657ae51d4510b71da69d 的一幅图, 它很形象的表达了 Proxy 的作用.
ES6 原生提供的 Proxy 构造函数, 用法如下:
var proxy = new Proxy(obj, handler)
其中 obj 为 Proxy 要拦截的对象, handler 用来定制拦截的操作, 返回一个新的代理对象 proxy;Proxy 代理特点:
Proxy 直接代理整个对象而非对象属性
Proxy 的代理针对的是整个对象, 而不是像 Object.defineProperty 针对某个属性. 只需做一层代理就可以监听同级结构下的所有属性变化, 包括新增属性和删除属性
Proxy 也可以监听数组的变化
例如上面 vue 使用的 Object.defineProperty 实现响应式方式用 Proxy 来实现则相对比较简单:
- let handler = {
- get(target, key){
- if (target[key] === 'object' && target[key]!== null) {
- // 嵌套子对象也需要进行数据代理
- return new Proxy(target[key], hanlder)
- }
- collectDeps() // 收集依赖
- return Reflect.get(target, key)
- },
- set(target, key, value) {
- if (key === 'length') return true
- notifyRender() // 通知订阅者更新
- return Reflect.set(target, key, value);
- }
- }
- let proxy = new Proxy(data, handler);
- proxy.age = 18 // 支持新增属性
- let proxy1 = new Proxy({arr: []}, handler);
- proxy1.arr[0] = 'proxy' // 支持数组内容变化
上面的 Proxy 的构造函数中的 get/set 为 Proxy 定义的 13 种的 trap 中的其中两种, 它共有 13 种代理操作方法:
trap | 描述 |
---|---|
handler.get | 获取对象的属性时拦截 |
handler.set | 设置对象的属性时拦截 |
handler.has | 拦截 propName in proxy 的操作,返回 boolean |
handler.apply | 拦截 proxy 实例作为函数调用的操作,proxy(args)、proxy.call(...)、proxy.apply(..) |
handler.construct | 拦截 proxy 作为构造函数调用的操作 |
handler.ownKeys | 拦截获取 proxy 实例属性的操作,包括 Object.getOwnPropertyNames、Object.getOwnPropertySymbols、Object.keys、for...in |
handler.deleteProperty | 拦截 delete proxy[propName] 操作 |
handler.defineProperty | 拦截 Objecet.defineProperty |
handler.isExtensible | 拦截 Object.isExtensible 操作 |
handler.preventExtensions | 拦截 Object.preventExtensions 操作 |
handler.getPrototypeOf | 拦截 Object.getPrototypeOf 操作 |
handler.setPrototypeOf | 拦截 Object.setPrototypeOf 操作 |
handler.getOwnPropertyDescriptor | 拦截 Object.getOwnPropertyDescriptor 操作 |
Proxy 代理目标对象, 是通过操作上面的 13 种 trap 来完成的, 这与 ES6 提供的另一个 apiReflect https://es6.ruanyifeng.com/#docs/reflect# 静态方法 的 13 种静态方法一一对应. 二者一般是配合使用的, 在修改 proxy 代理对象时, 一般也需要同步到代理的目标对象上, 这个同步就是用 Reflect 对应方法来完成的. 例如上面的 Reflect.set(target, key, value) 同步目标对象属性的修改. 需要补充一点:
13 种 trap 操作方法中, 若初始化时 handler 没设置的方法就直接操作目标对象, 不会走拦截操作
Proxy 的使用场景
Proxy 因为在目标对象之前架设了一层拦截, 外部对该目标对象的访问都必须经过这次拦截. 那么通过这层拦截, 可以做很多事情, 例如控制过滤, 缓存, 数据验证等等, 可以说 Proxy 的使用场景比较广, 下面简单列举几个使用场景, 更多实用场景可以参考 Proxy 的巧用 https://juejin.im/post/5d2e657ae51d4510b71da69d .
Vue3 的数据响应
vue3 中利用 Proxy 实现数据读取和设置时进行拦截, 在拦截 trap 中实现数据的依赖收集以及触发视图更新操作, vue3 该部分实现的主要伪码如下:
- 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
- }
获取属性对应的值, 无该属性或者属性为空返回默认值
在项目中经常遇到这样的需求, 在前端拿到后端返回的数据时, 获取某些可选字段时, 如果其值为空或者不存在该属性时, 可以设置一个默认值, 类似 loadsh 库的 get 方法_.get(object, path, [defaultValue]). 下面就对象形式下_.get 用 Proxy 来实现, 代码如下:
- function getValueByPath(object, path, defaultValue) {
- let proxy = new Proxy(object, {
- get(target, key) {
- if (key.startsWith('.')) {
- key = key.slice(1);
- }
- if (key.includes('.')) {
- path = path.split('.');
- let index = 0, len = path.length;
- while(target != null && index < len) {
- target = target[path[index++]]
- }
- return target || defaultValue;
- }
- if (!(key in target) || !target[key]) {
- return defaultValue
- }
- return Reflect.get(target, key)
- }
- });
- return proxy[path]
- }
需要注意的是, 参数 path 若有类似 a.b.c 这样嵌套的路径时, 我们是直接在 Proxy 的 handler.get 中处理的, 如果在 proxy 对象实例上调用如 proxy.a.b.c 则需要在 Proxy 的 handler.get 对返回对象的属性还需要创建其 Proxy 实例, 类似如下:
- function getValueByPath(object, path, defaultValue) {
- return proxy = new Proxy(object, {
- get(target, key) {
- if (isObject(target[key])){
- return new Proxy(target[key], {get(){}})
- }
- ... // 其他省略
- }
- })
- }
实现数组负数索引的访问
正常的数组, 如果访问数组的负数索引会得到 undefined, 现在要实现类似字符串的 substr 方法, 传递负数索引 index, 表示从倒数第 index 开始读取, 实现拦截如下:
- function getArrItem(arr) {
- return new Proxy(arr, {
- get(target, key, receiver) {
- let index = Number(key);
- if (index < 0) {
- key = String(target.length + index);
- }
- return Reflect.get(target, key, receiver)
- }
- });
- }
Proxy 的劣势
虽然 Proxy 相对于 Object.defineProperty 有很有优势, 但是并不是说 Proxy 就没有劣势, 这主要表现在以下两个方面:
兼容性问题, 无完全 polyfill
Proxy 为 ES6 新出的 API, 浏览器的对其支持情况可以在 https://caniuse.com/#search=Proxy 查到, 如下图所示:
可以看出虽然大部分浏览器支持 Proxy 特性, 但是一些浏览器或者其低版本不支持 Proxy, 其中 IE,QQ 浏览器, 百度浏览器等完全不支持, 因此 Proxy 有兼容性问题. 那能否像 ES6 其他特性那样有对应的 polyfill 解决方案呢, 答案并不那么乐观. 其中作为 ES6 转换的翘楚 babel, 在其官网 https://babeljs.io/docs/en/learn/#proxies 明确做了说明:
Due to the limitations of ES5, Proxies cannot be transpiled or polyfilled.
也就是说, 由于 ES5 的限制, ES6 的 Proxy 没办法被完全 polyfill, 所以 babel 没有提供对应的转换支持, Proxy 的实现是需要 JS 引擎级别提供支持, 目前大部分主要的 JS 引擎提供了支持, 可以查看 ES6 Proxy compatibilit.
然而, 截止目前 2019 年 10 月, Google 开发的 Proxy polyfill: https://github.com/GoogleChrome/proxy-polyfill , 其实现也是残缺的, 表现在:
只支持 Proxy 的 4 个 trap:get,set,apply 和 construct
部分支持的 trap 其功能也是残缺的, 如 set 不支持新增属性
该 polyfill 不能代理数组
性能问题
Proxy 的另一个就是性能问题, 为此有人专门做了一个对比实验, 原文在这里, 对应的中文翻译可以参考 ES6 Proxy 性能之我见. Proxy 的性能比 Promise 还差, 这就要需要在性能和简单实用上进行权衡. 例如 vue3 使用 Proxy 后, 其对对象及数组的拦截很容易实现数据的响应式, 尤其对数组来说.
另外, Proxy 作为新标准将受到浏览器厂商重点持续的性能优化, 性能这块相信会逐步得到改善.
参考文献
Proxy 的巧用 https://juejin.im/post/5d2e657ae51d4510b71da69d
ES6 Proxy 性能之我见
面试官: 实现双向绑定 Proxy 比 defineproperty 优劣如何? https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf
来源: https://www.cnblogs.com/wonyun/p/11699397.html