【Vue源码】Object.defineProperty与Proxy
Object.defineProperty() 和 ES2015 中新增的 Proxy 对象,会经常用来做数据劫持.
数据劫持:
在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果.数据劫持最典型的应用------双向的数据绑定(一个常用的面试题),
Vue 2.x 利用 Object.defineProperty(),并且把内部解耦为 Observer, Dep, 并使用 Watcher 相连
Vue 在 3.x 版本之后改用 Proxy 进行实现
1.Object.defineProperty()
Object.defineProperty() 的问题主要有三个:
-
- 不能监听数组的变化
- 必须遍历对象的每个属性
- 必须深层遍历嵌套的对象
不能监听数组的变化
数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse.
Vue 把会修改原来数组的方法定义为变异方法 (mutation method)
非变异方法 (non-mutating method):例如 filter, concat, slice 等,它们都不会修改原始数组,而会返回一个新的数组。
Vue 的做法是把这些方法重写来实现数组的劫持。
const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']; const arrayAugmentations = []; aryMethods.forEach((method)=> { // 这里是原生 Array 的原型方法 let original = Array.prototype[method]; // 将 push, pop 等封装好的方法定义在对象 arrayAugmentations 的属性上 // 注意:是实例属性而非原型属性 arrayAugmentations[method] = function () { console.log('我被改变啦!'); // 调用对应的原生方法并返回结果 return original.apply(this, arguments); }; }); let list = ['a', 'b', 'c']; // 将我们要监听的数组的原型指针指向上面定义的空数组对象 // 这样就能在调用 push, pop 这些方法时走进我们刚定义的方法,多了一句 console.log list.__proto__ = arrayAugmentations; list.push('d'); // 我被改变啦! // 这个 list2 是个普通的数组,所以调用 push 不会走到我们的方法里面。 let list2 = ['a', 'b', 'c']; list2.push('d'); // 不输出内容
必须遍历对象的每个属性
使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套
Object.keys(obj).forEach(key => { Object.defineProperty(obj, key, { // ... }) })
必须深层遍历嵌套的对象
当一个对象为深层嵌套的时候,必须进行逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。 Vue 的源码中这样的逻辑----walk 方法.
2.Proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
- 针对对象:针对整个对象,而不是对象的某个属性
- 支持数组:不需要对数组的方法进行重载,省去了众多 hack
- 嵌套支持: get 里面递归调用 Proxy 并返回
针对对象
不需要对 keys 进行遍历。这解决Object.defineProperty() 的第二个问题.Proxy 是针对整个 obj 的。所以 obj 内部包含的所有的 key ,都可以走进 set。(省了一个 Object.keys() 的遍历)
let obj = { name: 'Eason', age: 30 } let handler = { get (target, key, receiver) { console.log('get', key) return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { console.log('set', key, value) return Reflect.set(target, key, value, receiver) } } let proxy = new Proxy(obj, handler) proxy.name = 'Zoe' // set name Zoe proxy.age = 18 // set age 18
另外 Reflect.get 和 Reflect.set 可以理解为类继承里的 super,即调用原来的方法
Reflect.get():获取对象身上某个属性的值,类似于 target[name]。
Reflect.set():将值分配给属性的函数,返回一个Boolean,如果更新成功,则返回true。
支持数组
let arr = [1,2,3] let proxy = new Proxy(arr, { get (target, key, receiver) { console.log('get', key) return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { console.log('set', key, value) return Reflect.set(target, key, value, receiver) } }) proxy.push(4) // 能够打印出很多内容 // get push (寻找 proxy.push 方法) // get length (获取当前的 length) // set 3 4 (设置 proxy[3] = 4) // set length 4 (设置 proxy.length = 4)
嵌套支持
Proxy 也是不支持嵌套的,这点和 Object.defineProperty() 是一样的。因此也需要通过逐层遍历来解决。Proxy 的写法是在 get 里面递归调用 Proxy 并返回
let obj = { info: { name: 'eason', blogs: ['webpack', 'babel', 'cache'] } } let handler = { get (target, key, receiver) { console.log('get', key) // 递归创建并返回 if (typeof target[key] === 'object' && target[key] !== null) { return new Proxy(target[key], handler) } return Reflect.get(target, key, receiver) }, set (target, key, value, receiver) { console.log('set', key, value) return Reflect.set(target, key, value, receiver) } } let proxy = new Proxy(obj, handler) // 以下两句都能够进入 set proxy.info.name = 'Zoe' proxy.info.blogs.push('proxy')
其它方面
优势:Proxy 的第二个参数可以有 13 种拦截方法,比 Object.defineProperty() 要更加丰富,Proxy 作为新标准受到浏览器厂商的重点关注和性能优化,相比之下 Object.defineProperty() 是一个已有的老方法。
劣势:Proxy 的兼容性不如 Object.defineProperty() (caniuse 的数据表明,QQ 浏览器和百度浏览器并不支持 Proxy,这对国内移动开发来说估计无法接受,但两者都支持 Object.defineProperty()),不能使用 polyfill 来处理兼容性
3.应用
3.1上面试题一道:
什么样的 a 可以满足 (a === 1 && a === 2 && a === 3) === true 呢?(注意是 3 个 =,也就是严格相等)
解决:每次访问 a 返回的值都不一样,那么肯定会想到数据劫持(有其它解法)
let current = 0 Object.defineProperty(window, 'a', { get () { current++ console.log(current) return current } }) console.log(a === 1 && a === 2 && a === 3) // true
3.2多继承
Javascript 通过原型链实现继承,正常情况一个对象(或者类)只能继承一个对象(或者类)。但通过这两个方法允许一个对象继承两个对象。
let foo = { foo () { console.log('foo') } } let bar = { bar () { console.log('bar') } } // 正常状态下,对象只能继承一个对象,要么有 foo(),要么有 bar() let sonOfFoo = Object.create(foo); sonOfFoo.foo(); // foo let sonOfBar = Object.create(bar); sonOfBar.bar(); // bar // 黑科技开始 let sonOfFooBar = new Proxy({}, { get (target, key) { return target[key] || foo[key] || bar[key]; } }) // 我们创造了一个对象同时继承了两个对象,foo() 和 bar() 同时拥有 sonOfFooBar.foo(); // foo 有foo方法,继承自对象foo sonOfFooBar.bar(); // bar 也有bar方法,继承自对象bar
转自:https://blog.csdn.net/qq_42833001/article/details/83302149