Vue2和Vue3响应式的实现原理

Vue2响应式原理

实现Vue2响应式原理主要就是通过数据劫持,依赖收集,派发更新的方式来实现的。

  • 数据劫持:使用 Object.defineProperty 方法添加对象,重写了原有的 get 和 set 方法;

  • 依赖收集:在渲染视图时 将watcher和具体的属性,通过发布订阅者模式管理,这样数据改变之后就能更精准的更新视图,也就是需要用到数据的地方,称为依赖;

  • 派发更新:通过dep来执行watcher的notify方法。

在 getter() 中收集依赖,在 setter() 中触发依赖	// 对象类型
在 getter() 中收集依赖,在 拦截器 中触发依赖		// 数组类型

使用Object.defineProperty做响应式的缺点:

  • 深度监听,需要一次性递归到底,计算量比较大,如果对象中属性过多,那么需要给每个属性都绑定defineProperty,十分损耗效率。
  • 只能监听对象属性的修改、读取,无法监听到对象属性的动态添加和删除;
  • 无法原生监听数组,通过下标、length修改数组也不会响应式刷新页面,可以直接重新赋值,或者使用filter、map、concat、slice等生成新数组对其赋值。

解决方案:使用this.$set 、this.$delete

this.$set(this.arr, index, value)
Vue.set(this.arr, index, value)
    
this.$delete(this.arr, index)
Vue.delete(this.arr, index)

// 操作数组的函数
splice(),push(), pop(), shift(), unshift(), sort(), reverse()

相应式实例代码:

let person = {
  name:'anna',
  age:18
}

let p = {}
Object.defineProperty(p, "name", {
  // 获取 name 时调用
  get(){      
    return person.name;
  },
  // 设置 name 时调用
  set(value){      
    console.log("修改 name 属性")
    person.name = value
  }
});

Object.defineProperty(p, "age", {
  // 获取 age 时调用
  get(){      
    return person.age;
  },
  // 设置 age 时调用
  set(value){      
    console.log("修改 age 属性")
    person.age = value
  }
});

此时,p对象就完成了对person对象的代理,当读取p.name时,实际上是在读取person.name,当修改p.name时,实际上person中name属性的值也会随之更新。

但是,在Vue2中,无法通过p对象对person对象进行增和删的操作,实际上person对象是捕获不到的,所以即便通过p对象删除和增加属性,person对象内的属性是不会更新的。

基于Vue2的缺点,在Vue3 中得到了解决。

Vue3中的响应式原理

和Vue2不同的是,它的核心是es6的 Proxy 结合 Reflect 实现的,使用代理,本质上是通过 `Proxy` 劫持了数据对象的读写,当我们访问数据时,会触发 getter 执行依赖收集;修改数据时,会触发 setter 派发通知;这样就可以不直接操作对象,也可以监听到所有对象的增删改查,同时也提升了效率。

扫描二维码关注公众号,回复: 14766662 查看本文章

Proxy:

es6新增了Proxy类,即代理的作用。如果要监听一个对象,可以先创建代理对象,之后对对象的所有操作,都由代理对象完成,它可以监听我们的所有操作。支持的拦截操作共 `13` 种。

// 基本使用
const p = new Proxy( target, handler );
target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

let person = {
  name: "anna",
  age: 18,
};

let p = new Proxy(person, {
  get(target, key) {
    return target[key];
  },
  set(target, key, val) {
    return (target[key] = val);
  },
});
console.log(p);
//测试 get 是否可以拦截成功
console.log(p.name); // 输出 anna
console.log(p.age); // 输出 18
console.log(p.job); // 输出 undefined
//测试 set 是否可以拦截成功
p.age = 25;
console.log(p.age);

Proxy代理的是整个对象,而不是对象的某个特定属性,不需要我们通过遍历来逐个进行数据绑定。

Reflect:

`Reflect`对象与`Proxy`对象一样,也是 ES6 为了操作对象而提供的新 API,它提供拦截 JavaScript 操作的方法,这些方法与 Proxy 的方法一一对应,也是 `13` 种。Reflect就是为了让this指向代理对象。

let person = {
  name: "anna",
  age: 18,
};
const objProxy = new Proxy(person, {
  get(target, key, receiver) {
    console.log("属性被访问");
    return Reflect.get(target, key)
   },
   set(target, key, newValue, receiver) {
     console.log("属性被修改");
     Reflect.set(target, key, newValue)
   }
})
objProxy.name = 'lily'
objProxy.age = 25
console.log(objProxy.name);
console.log(objProxy.age);

基本数据类型通过ref实现响应式,引用数据类型通过reactive实现响应式,可以拦截对象中任意操作的变化,包括属性的读写、属性添加、属性删除,以及数组下标的修改。

setup() {
    // 没有响应式
    let msg = "hello";
    function changeMsg() {
      msg = "hello world";
    }
    // 通过 ref 定义响应式变量:基本数据类型
    // ref() 返回带有 value 属性的对象
    let counter = ref(0);
    function changeCounter() {
      counter.value++;
    }

    // 通过 reactive 定义引用类型的数据:对象和数组,引用类型数据
    let obj = reactive({
      name: "anna",
      age: 18,
      person: {
        name: "lily",
      },
    });
    function changeObj() {
      obj.person.name = "bob";
      obj.name = "bob";
    }

    // toRefs 使解构后的数据重新获得响应式
    // 通过 es6 扩展解构运算符进行解构使得对象中的属性不是响应式的
    return {
      msg,
      changeMsg,
      counter,
      changeCounter,
      obj,
      changeObj,
      ...toRefs(obj),
    };
},
<template>
  <div class="hello">
    <h1>{
   
   { msg }}</h1>
    <h1>hello</h1>
    <h1>{
   
   { counter }}</h1>
    <h1>{
   
   { obj.person.name }}</h1>
    <h1>{
   
   { obj.name }}</h1>
    <h1>{
   
   { person.name }}</h1>
    <h1>{
   
   { name }}</h1>
    <button @click="changeMsg">改变msg</button>
    <button @click="changeCounter">改变counter</button>
    <button @click="changeObj">改变obj</button>
  </div>
</template>

利用Proxy实现数据劫持的优势:

  • 解决了使用 Object.defineProperty() 数据劫持的缺点,完美监听任何方式导致的数据改变行为;
  • 对于整个拦截对象直接进行挟持,无需遍历属性依次添加定义get和set属性;
  • 对于原对象生成拦截对象,然后对拦截对象进行相应监听行为,确保原对象不变。

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 new Proxy() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

Vue 3.0与Vue 2.0的区别仅是数据劫持的方式由 Object.defineProperty 更改为Proxy代理。

Object.defineProperty() 和 Proxy 的区别

Proxy

优点:

  • 可以直接监听对象而非属性;
  • 可以直接监听数组的变化;
  • 有13种拦截方法(Object.defineProperty() 没有);
  • 返回的是一个新对象,我们可以直接操作新对象来达到目的,而Object.defineProperty() 只能遍历对象属性直接修改;
  • 性能优化。

缺点:

  • 无法兼容所有浏览器,比如IE11,无法进行polyfill。

Object.defineProperty()

优点:

  • 兼容性较好

缺点:

  • 无法对数组进行监听,采用的是对数组的方法进行重写(push、pop、shift、unshift等),对此进行双向绑定和数据监听的操作;
  • 效率差,因为对多层数据进行一次性的递归操作,当数组多时,非常影响性能。

总结:为什么vue3响应式优于vue2响应式

  • Vue2的响应式是基于`Object.defineProperty`实现的
  • Vue3的响应式是基于ES6的`Proxy+Reflect`来实现的

vue2中存在的问题

  • 需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题
  • 对象的新增属性、删除属性、界面不会更新
  • vue2的解决方法: 通过vm.$set(obj,key.val)/vm.$delete(obj,key)新增属性/删除属性
  • 直接通过下标修改数组,界面不会自动更新
  •  vue2的解决方法:vm.$set(arr,index.value),调用数组的splice方法

vue3中的优化

vue2是监听对象的属性,vue3是监听对象

  • 不需要一次性遍历data的属性,可以显著提高性能。需要的时候再进行深度监听。
  • 基于Proxy,对被代理对象设置拦截,访问或操作被代理对象都要先通过拦截。所以可以监听对象属性的添加和删除。
  • 可以原生监听数组,不需要通过重写方法来实现对数组的监控。

猜你喜欢

转载自blog.csdn.net/qq_43641110/article/details/129859769