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.aObject.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] = valconsole.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 的方式更加现代化和优雅,提升了整体的性能和可维护性。