看完就懂的vue2与vue3响应式的区别

一、前言

数据响应式
所谓数据响应式就是建立响应式数据与依赖(调用了响应式数据的操作)之间的关系,当响应式数据发生变化时,可以通知那些使用了这些响应式数据的依赖操作进行相关更新操作,可以是DOM更新,也可以是执行一些回调函数。

从Vue2到Vue3都使用了响应式,那么它们之间有什么区别?

  • Vue2响应式:基于Object.defineProperty()实现的。
  • Vue3响应式:基于Proxy实现的。

那么它们之间有什么区别?为什么Vue3会选择Proxy替代defineProperty?

请听我娓娓道来~~
在这里插入图片描述
重要插一句!!!!
defineProperty实际上是对象里面基本方法之一,而proxy是针对整个对象所有基本方法的拦截器。这是最本质的区别!!!!

二、Object.defineProperty()

1、Object.defineProperty

Object.defineProperty(obj, prop, descriptor) 方法会直接在一个对象上定义一个新属性,或修改一个对象的现有属性,并返回此对象,其参数具体为:

obj:要定义属性的对象

prop:要定义或修改的 属性名称 或 Symbol

descriptor:要定义或修改的 属性描述符

从以上的描述就可以看出一些限制,比如: 目标是 对象属性,不是 整个对象 一次只能 定义或修改一个属性
当然有对应的一次处理多个属性的方法Object.defineProperties(),但在 vue 中并不适用,因为 vue不能提前知道用户传入的对象都有什么属性,因此还是得经过类似 Object.keys() + for 循环的方式获取所有的 key ->value,而这其实是没有必要使用 Object.defineProperties()

2、为什么使用Object.defineProperty

const obj = {
    a: 1,
    b: 2,
    c: {
        a: 1,
        b: 2
    }
}
// obj.a
// obj.a = 3

首先思考:vue的响应式到底要干什么?无非就是做一件事,就是当我们读这个对象属性的时候,我们要知道它读了,我要做些别的事我要插一脚。当给它重新赋值的时候,我要知道它在重新赋值,我要插一脚。

上面代码现在这脚就插不进去,所以要想一个办法,把这个属性的读取和赋值变成一个函数,希望将来读这个属性的时候,运行这么个函数,给这个属性赋值的时候,把新的值传给我。一变函数就简单了,不要说插一脚,100脚都不是问题。

那怎么变成函数呢,在ES6之前,没有别的办法。只有Object.defineProperty()

const obj = {
    a: 1,
    b: 2,
    c: {
        a: 1,
        b: 2
    }
}

let v = obj.a // 拿到原始值
Object.defineProperty(obj, 'a', {
    get () { // 读的时候 运行get
        console.log('a', '读取')
        return v
    },
    set (val) { // 赋值的时候,运行set
        if (val !== v) {
            console.log('a', '更改')
            v = val
        }
    }
})
obj.a
obj.a = 3

由于vue2是针对属性的监听,所以就必须要去深度遍历每一个属性,所以在vue2里面就有一个observe(),要玩这个对象之前,先对它进行监听。

const obj = {
    a: 1,
    b: 2,
    c: {
        a: 1,
        b: 2
    }
}
// 判断是不是Object
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 () { // 读的时候 运行get
		        console.log('k', '读取')
		        return v
		    },
		    set (val) { // 赋值的时候,运行set
		        if (val !== v) {
		            console.log('k', '更改')
		            v = val
		        }
		    }
		})
	}
}


obj.a
obj.a = 3
obj.bbbbb = 666 // 没有被监听到
delete obj.a // 没有被监听到

在vue2里面观察的方式就是深度遍历每一个属性,把每一个属性的读取和赋值变成函数,只要变成函数,就可以插一脚。具体这一脚是做啥,再次先不讨论哈。

这就是vue2的做法,但是有一个天生的缺陷

由于它是针对每个属性的监听,所以他就必须要进行深度的遍历,这会有效率的损失。由于在观察这个步骤里面,它完成了深度遍历,在观察这个步骤时间点,有的属性都被监听到了,都被改成了get和set函数了,观察这一步做完后,再去新增属性,就不知道了,对这个属性而言,是没有被监听的。

我们学vue生命周期,在created()之前就完成了监听。后修再添加属性,它就不知道了,这就是为什么vue2无法监听属性的新增、删除。

我们再整理下:

defineReactive(data,key,val){
    Object.defineProperty(data,key,{
      enumerable:true,
      configurable:true,
      get:function(){
        console.log(`对象属性:${key}访问defineReactive的get!`)
        return val;
      },
      set:function(newVal){
        if(val===newVal){
          return;
        }
        val = newVal;
        console.log(`对象属性:${key}访问defineReactive的get!`)
      }
    })
}
let obj = {};
this.defineReactive(obj,'name','sapper');
// 修改obj的name属性
obj.name = '工兵';
console.log('obj',obj.name);
// 为obj添加age属性
obj.age = 12;
console.log('obj',obj);
console.log('obj.age',obj.age);
// 为obj添加数组属性
obj.hobby = ['游戏', '原神'];
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

// 为obj添加对象属性
obj.student = {school:'大学'};
obj.student.school = '学院';
console.log('obj.student.school',obj.student.school);

在这里插入图片描述
从上图可以看出使用defineProperty定义了包含name属性的对象obj,然后添加age属性、添加hobby属性(数组)、添加student属性并分别访问,都没有触发obj对象中的get、set方法。

也就是说defineProperty定义对象不能监听添加额外属性或修改额外添加的属性的变化,我们再看看这样一个例子:

let obj = {};
// 初始化就添加hobby
this.defineReactive(obj,'hobby',['游戏', '原神']);
// 改变数组下标0的值
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

在这里插入图片描述

假如我们一开始就为obj添加hobby属性,我们发现修改数组下标0的值,并没有触发obj里的set方法

也就是说defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化。

Object.defineProperty():

  • defineProperty定义对象不能监听添加额外属性或修改额外添加的属性的变化
  • defineProperty定义对象不能监听根据自身数组下标修改数组元素的变化

3、Object.defineProperty 拦截 Array

Object.defineProperty 可用于实现对象属性的 get 和 set 拦截,而数组其实也是对象,那自然是可以实现对应的拦截操作,如下:
在这里插入图片描述
Vue2 为什么不使用 Object.defineProperty 拦截 Array?
尤大在曾在 GitHub 的 Issue 中做回复: 我是因为性能问题

  • 数组 和 普通对象 在使用场景下有区别,在项目中使用数组的目的大多是为了 遍历,即比较少会使用 array[index] = xxx 的形式,更多的是使用数组的 Api 的方式
  • 数组长度是多变的,不可能像普通对象一样先在 data 选项中提前声明好所有元素,比如通过 array[index] = xxx 方式赋值时,一旦 index 的值超过了现有的最大索引值,那么当前的添加的新元素也不会具有响应式
  • 数组存储的元素比较多,不可能为每个数组元素都设置 getter/setter
  • 法拦截数组原生方法如 push、pop、shift、unshift 等的调用,最终仍需 重写/增强 原生方法

4、缺陷

在这里插入图片描述

三、Proxy

无论是vue2还是vue3,都必须把读取和赋值变成函数,这是必须的,不玩玩不了。
只不过变函数的方式不一样,在vue3里面就不会针对这些属性进行监听了,而是直接监听整个对象。那这就简单了,都不需要遍历了。只要在动这个对象就能收到通知,那么是怎么做到的呢?就是proxy。

这样不管是读的哪一个属性,给属性重新赋值的时候,也会收到通知。这样就会产生一个代理对象。使用这个属性都是通过这个代理对象去做的。

const obj = {
    a: 1,
    b: 2,
    c: {
        a: 1,
        b: 2
    }
}
// 观察
new Proxy(obj, {
	get (target, k) { // 读的时候 运行get
		let v = target[k]
        console.log('k', '读取')
        return v
    },
    set (target, k, val) { // 赋值的时候,运行set
        if (target[k] !== val) {
            console.log('k', '更改')
            target[k] = val
        }
    },
    deleteProperty(){ // 删除属性监听
	
	}
})

proxy.a = 3
proxy.b
proxy.ccccccccc

由于它不去监听属性了,就不需要遍历了,监听的是整个对象,所以之后对属性的操作,都是能收到通知的。
虽然这个代码和vue的源码还有很多细节上的差别,但是核心道理就是如此。

我们再整理下:

const obj = {
    a: 1,
    b: 2,
    c: {
        a: 1,
        b: 2
    }
}

// 判断是不是Object
function isObject(v) {
	return typeof v === 'object' && v !==  null
}

// 观察
function observe(obj){
	const proxy = new Proxy(obj, {
		get (target, k) { // 读的时候 运行get
			console.log('k', '读取')
			let v = target[k]
	        if (isObject(v)) { // 虽然是递归,但是不会影响一开始的效率
	        	v = observe(v)
	        }
	        return v
	    },
	    set (target, k, val) { // 赋值的时候,运行set
	        if (target[k] !== val) {
	            console.log('k', '更改')
	            target[k] = val
	        }
	    },
	    deleteProperty(){ // 删除属性监听
		
		}
	})
	return proxy 
}
const proxy = observe(obj)

proxy.a = 3
proxy.b
proxy.ccccccccc

再看看例子:

// proxy实现
let targetProxy = {name:'sapper'};
let objProxy = new Proxy(targetProxy,{
    get(target,key){
      console.log(`对象属性:${key}访问Proxy的get!`)
      return target[key];
    },
    set(target,key,newVal){
      if(target[key]===newVal){
        return;
      }
      console.log(`对象属性:${key}访问Proxy的set!`)
      target[key]=newVal;
      return target[key];
    }
})
// 修改objProxy的name属性
objProxy.name = '工兵';
console.log('objProxy.name',objProxy.name);
// 为objProxy添加age属性
objProxy.age = 12;
console.log('objProxy.age',objProxy.age);
// 为objProxy添加hobby属性
objProxy.hobby = ['游戏', '原神'];
objProxy.hobby[0] = '王者';
console.log('objProxy.hobby',objProxy.hobby);
// 为objProxy添加对象属性
objProxy.student = {school:'大学'};
objProxy.student.school = '学院';
console.log('objProxy.student.school',objProxy.student.school);

在这里插入图片描述
从上图是不是发现了Proxy与defineProperty的明显区别之处了,Proxy能支持对象添加或修改触发get、set方法,不管对象内部有什么属性。

我们再看看Vue里的用法例子:

 data() {
   return {
     name: 'sapper',
     student: {
       name: 'sapper',
       hobby: ['原神', '天涯明月刀'],
     },
   };
 },
 methods: {
   deleteName() {
     delete this.student.name;
     console.log('删除了name', this.student);
   },
   addItem() {
     this.student.age = 21;
     console.log('添加了this.student的属性', this.student);
   },
   updateArr() {
     this.student.hobby[0] = '王者';
     console.log('更新了this.student的hobby', this.student);
   },
}

在这里插入图片描述
从图中确实可以修改data里的属性,但是不能及时渲染,所以Vue2提供了两个属性方法解决了这个问题:Vue. s e t 和 V u e . set和Vue. setVue.delete。注意不能直接this._ data.age这样去添加age属性,也是不支持的。

this.$delete(this.student, 'name');// 删除student对象属性name
this.$set(this.student, 'age', '21');// 添加student对象属性age
this.$set(this.student.hobby, 0, '王者');// 更新student对象属性hobby数组

在这里插入图片描述

const user = {name:'张三'}
const obj = new Proxy(user,{
  get:function (target,key){
    console.log("get run");
    return target[key];
  },
  set:function (target,key,val){
    console.log("set run");
    target[key]=val;
    return true;
  }
})
obj.age = 22;
console.log(obj); // 监听对象添加额外属性打印set run!  
const obj = new Proxy([2,1],{
  get:function (target,key){
    console.log("get run");
    return target[key];
  },
  set:function (target,key,val){
    console.log("set run");
    target[key]=val;
    return true;
  }
})
obj[0] = 3;
console.log(obj); // 监听到了数组元素的变化打印set run!  
  • Proxy:解决了上面两个弊端,proxy可以实现:
  • 可以直接监听对象而非对象属性,可以监听对象添加额外属性的变化;
  • 可以直接监听数组的变化
  • Proxy 返回的是一个新对象,而 Object.defineProperty 只能遍历对象属性直接修改。
  • 支持多达13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具备的。

四、整理下Proxy代理 看这里 更好理解

// 创建响应式
function reactive(target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组 则返回
    return target
  }

  // 代理配置
  const proxyConf = {
    get(target, key, recriver) {
      // 只处理本身(非原型的)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key) // 监听
      }
      const result = Reflect.get(target, key, recriver)
      // return result // 返回结果
      return reactive(result) // 递归 深度监听 提升性能  !!!!!关键
    },
    set(target, key, val, receiver) {
      // 重复数据,不处理
      if (target[key] === val) {
        return true
      }
      // 判断是不是新增
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的key', key)
      } else {
        console.log('新增的key', key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      return result // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return result // 是否设置成功
    }
  }

  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}

// 测试数据
const data = {
  name: 'zhangmin',
  age: 25,
  arr: [1,2],
  info: {
    city: 'ganzhou',
    a: {
      b: 'c'
    }
  }
}

const proxyData = reactive(data)

// 总结: 深度监听,性能更好
// 可以监听 新增/删除 属性
// 可监听数组变化

// 能规避Object.definePropety的问题
// 缺陷: 无法兼容所有的浏览器,无法polyfill