1、思考
Vue的响应式到底要干什么?
- 无非就是要知道当你读取对象的时候,要知道它读了。要做一些别的事情
- 无非就是要知道当你修改对象的时候,要知道它改了。要做一些别的事情
- 所以要想一个办法,把读取和修改的动作变成一个函数,读取和修改的时候分别调用对应的函数
在ES6之前,只能通过Object.defineproperty 给它变成一个get和set函数。当读取这个属性的时候运行get,修改这个属性的时候运行set
在ES6之后,就能通过Porxy去代理整个对象
2、Vue2的做法
针对某个对象中某个属性的做法
通过Object.defineProperty去针对某个对象的属性去进行监听
const obj = { a: 1, b: 2, c: { d: 3, e: 4, }, } // 保存初始值 let v = obj.a Object.defineProperty(obj, 'a', { get() { console.log('a', '读取') return v }, set(val) { // 当原来的值与重新赋值的值不一样的时候才进行修改 if (val !== v) { console.log('a', '更改了') v = val } }, })
当我们去读取 obj.a 这个属性的时候 get 函数 就会调用。
当我们去修改 obj.a 这个属性的时候 set 函数 就会调用。
针对某个对象中多个属性的做法
在Vue2的源码中,有一个函数叫做 observe(观察器)。在这个函数中,去深度遍历对象中的每一个属性,给每一个属性添加 Object.defineProperty,这样就能对对象中的每一个属性叫做监听。这个过程就叫做 观察
const obj = { a: 1, b: 2, c: { d: 3, e: 4, }, } // 辅助函数 判断这个值是不是一个对象 function _isObject(v) { return typeof v === 'object' && v !== null } function observe(obj) { for (const k in obj) { let v = obj[k] // 如果这一个属性仍然是一个对象的话,就需要深度遍历 if (_isObject(v)) { observe(v) } Object.defineProperty(obj, k, { get() { console.log(k, '读取了') return v }, set(val) { // 当原来的值与重新赋值的值不一样的时候才进行修改 if (val !== v) { console.log(k, '更改了') v = val } }, }) } } observe(obj)
当我们去读取对象中的某个属性的时候 get 函数 就会调用。
当我们去修改对象中的某个属性的时候 set 函数 就会调用。
打印内容解释:
- obj.a = 3 更改
- obj.c.d = 4 先读取 obj.c 的值,再更改 obj.c.d 的值
- obj.c.e 先读取 obj.c 的值,再读取 obj.c.e 的值
总结
- 在Vue2里面观察的方式就是 深度遍历每一个属性 把每一个属性的读取和赋值变成函数get和set。
- 在这种做法下有一个天生的缺陷,由于它是针对每个属性的监听,所以就必须要进行深度的遍历,这样会有效率损失。
- 由于在 observe(obj) 观察这个步骤里边完成了深度遍历,也就是说在这个时间点里边,这些属性被我们监听到了都被改成get和set了
但是这一步一旦做完了之后。再去新增的话它就不知道了。比如 obj.qwertr = 3 对于这个属性而言,它就是没有监听的,因为监听的步骤已经结束了
这就是为什么Vue2它无法监听属性的新增,当然也包括属性的删除。它也收不到通知。因为在 Object.defineProperty 既不会运行get也不会运行set
3、Vue3的做法
其实核心道理都是一样的。无论是Vue2还是Vue3,都必须要把读取和赋值变成函数。只不过Vue3变成函数的方式不一样。在Vue3里面不会去对对象的每一个属性进行监听了,而是直接监听整个对象。将来不管是在这个对象中添加还是删除属性都不怕了。因为监听的是整个对象,这要动了这个对象就能收到通知。
做法
Vue3使用的是Proxy
const obj = { a: 1, b: 2, c: { d: 3, e: 4, }, } const proxy = new Proxy(obj, { // 读这个对象的属性的时候收到通知 get(target, k) { // target就是obj,k是属性名 let v = target[k] console.log(k, '读取') return v }, // 修改这个对象的属性的时候收到通知 set(target, k, val) { // target就是obj,k是属性名,val是新值 if (target[k] !== val) { target[k] = val console.log(k, '更改') return target[k] } }, // 删除对象的属性的时候收到通知 deleteProperty(target, k, val) { console.log(k, '删除') return target[k] }, })
会产生一个代理对象propx,将来去读属性也好,重新给属性赋值也好都是通过这个代理对象去做的。
总结
- Proxy 对象可以直接代理整个对象,而不需要遍历对象属性进行劫持,这样可以减少运行时的性能开销。在 Vue 2 中,由于每个属性都需要单独设置 get 和 set,对于大量的属性或嵌套属性,这种劫持可能会导致性能下降。
- 另外,使用 Proxy 的方式更符合现代 JavaScript 的发展趋势,更好地利用了 JavaScript 引擎的优化。
4、总结
综上所述,Vue 3 中使用 Proxy 对象代替了 Vue 2 中的
Object.defineProperty
,带来了更强大、更灵活和更高效的属性拦截和代理功能,同时也提升了开发体验和调试效率。这些改进使得 Vue 3 在处理 Props 的方式更加现代化和优雅,提升了整体的性能和可维护性。