Vue2和Vue3响应式原理

Vue2和Vue3响应式原理

  • 前言
  • vue2 响应式原理
    • Object.defineProperty()
    • 定义:
    • 原理:
    • 基本用法:
    • 目标:
    • 代码及注解
  • vue3 响应式原理
    • 原理
    • Proxy
    • 原理实现
    • 创建简单的响应式对象
    • 为什么要使用Reflect()?
    • 什么是Reflect()?
    • 简单的响应式对象可能会出现的问题
    • 优化后的响应式对象
  • Vue2、Vue3响应式原理的区别
    • 区别1:
    • 区别2:
  • 总结

前言

  • 响应式原理就是指的是MVVM的设计模式的核心,即数据驱动页面,一旦数据改- 变,视图也会跟着改动。
  • vue2的响应式原理是由Object.defineProperty()实现的 (数据劫持)
  • vue3的响应式原理是由es6中的Proxy所实现的 (数据代理)

vue2 响应式原理

Object.defineProperty()

定义:

直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。(应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。)

原理:

对象类型:通过Object.defineProperty()对属性的读取,修改进行拦截(数据劫持)。通过里面的gettersetter方法,进行查看和数据的修改,通过发布、订阅者模式进行数据与视图的响应式。
数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

基本用法:

Object.defineProperty(obj, prop, descriptor), 它接收三个参数

  • obj: 要定义属性的对象。它只接收对象
  • prop: 要定义或修改的属性的名称或 Symbol 。
  • descriptor: 要定义或修改的属性描述符。
    descriptor是一个对象,它定义和修改指定的属性,它包含以下的键值,来对原对象进行数据劫持,即对象会执行这里面的逻辑。
    configurable、enumerable 、 writable 、 value、get 、 set.
    拥有布尔值的键 configurable、enumerable 和 writable 的默认值都是 false。
    属性值和函数的键 value、get 和 set 字段的默认值为 undefined。

目标:

数据变了,视图就要更新。

代码及注解

	let oldArrayPrototype = Array.prototype;
    let proto = Object.create(oldArrayPrototype);
    // 如果要监听的对象是一个数组,我们又该怎么办呢。
    //因为我们明确直到Object.defineProperty()的target只能是对象,如果是数组,
    //好像Object.defineProperty()无法实现,那我们应该怎么办。
    //而在vue2中,当我们改变数组时,视图也会变化,说明我们也要实现数组的响应式。
    //这里我们需要换一个思路,我们去重写一下数组原型上的方法,我们把和数组有关的所有api,
    //比如push、pop、shift、reserve ...全部重写一遍。
    //当我们在执行这些操作的时候,同时把视图更新的操作也完成。这样就可以了。
    //这里你可能会问,我们可以去修改数组的源码吗。诶,当然可以。实际上vue2也就是这么操作的。

    ['push', 'shift', 'unshift'].forEach(method => {
        // 函数劫持,重写函数
        proto[method] = function () {
            updateView()
            oldArrayPrototype[method].call(this, ...arguments)
        }
    })
    // 重写数组方法 push shift unshift pop reserve ...


    function defineReactive(target, key, value) {
        if (typeof value == 'object' && value !== null) {
            observer(value)
        }

        // get()就是简单的取值,直接把这个属性对应的值返回就好了,
        //set()就是当我们要修改值得时候,它接收一个参数就是我们修改的新值newValue,将要执行的逻辑,
        //我们的目的就是要在修改值得时候将视图更新,于是在这里直接调用updataView()这个函数就好了,
        //然后我们把新值赋值上去。这里做了一个小小的优化就是,如果新值刚好等于老值,
        //我们就不需要去更新视图。于是最简陋的vue2响应式原理就实现了。
        Object.defineProperty(target, key, {
            get() {
                return value
            },

            //如果我们要劫持的对象内部嵌套对象,当我们改变hobbies内部的属性的时候,
            //视图依然不会更新,这里只能用到递归来解决。 
            set(newValue) {
                if (newValue !== value) {
                    updateView()
                    value = newValue
                }
            }
        })
    }
    // 首先,我们需要有一个观察者,在vue2源码中也有这个,它的目的是用于判断,
    //我们要定义或修改的target是不是一个对象。
    //如果是对象,我们用Object.defineProperty()对它进行数据劫持。如果不是我们就直接返回target.
    function observer(target) { // 观察者
        if (typeof target !== 'object' || target == null) {
            return target
        }

        if (Array.isArray(target)) {
            // Object.setPrototypeOf(target, proto)
            target.__proto__ = proto
        }

        for (let key in target) {
            // defineReactive(target, key, target[key]) ,
            //这个方法就是来调用Object.defineProperty的。
            //target是我们要定义或修改的对象,key是要修改或定义的属性,target[key]就是这个属性的值。
            defineReactive(target, key, target[key])
        }
    }

    function updateView() {
        console.log('更新视图');
    }
    let data = {
        name: '老王',
        hobbies: {
            a: '喝酒',
            b: '抽烟'
        },
        job: ['driver', 'coder', 'cooker']
    }
    observer(data)
    // console.log(dat)
    console.log(data.hobbies.a);
    data.hobbies.a = '烫头'
    console.log(data.hobbies.a);
    console.log(data.job)
    data.job.push('teacher')
    console.log(data.job)

vue3 响应式原理

原理

  • 通过Proxy(代理): 拦截对象中任意属性的变化,包括:属性值的读写,属性的增加,属性的删除等。

  • 通过Reffect(反射): 对源对象的属性进行操作

new Proxy(data,{
  //拦截读取属性值
  get(target, prop){
    return Reflect.get(target, prop)
  },
  //拦截设置属性值或添加新属性
  set(target, prop, value){
    return Reflect.set(target, prop, value)
  },
  //拦截删除属性
  deleteProperty(target, prop){
    return Reflect.deleteProperty(target, prop)
  }
})

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

基本用法
Proxy 接收两个参数,target, handler

  • target

要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。

  • handler

一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。

  • handler get set 属性
    • get()
      get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
    • set()
      set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

这里先提一下WeakMap,WeakSet:
1.传统的Object只能以字符串作为键名,这具有非常大的限制
2.于是有了map数据结构,它可以用任意数据类型作为键名,有get和set方法,用于取值和添加键值对
3.WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。 WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size属性。
4.Set是ES6 提供的新的数据结构 。它类似于数组,但是成员的值都是唯一的,没有重复的值。有add和delete方法,用于添加和删除。还有size属性,用于获取长度
5.WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用。

WeakMap,WeakSet都是弱引用(具体是WeakMap中的key是弱引用)。

解释: 一般我们将数据存在堆当中,需要我么手动去把这些数据清除这些数据的引用。将它们设置为null,很容易造成内存泄漏。然而如果我们使用WeakMap或者WeakSet,WeakMap中的键和WeakSet一旦没有其他对象的引用时,他会消失,也就是会自动被垃圾回收自动回收。

原理实现

目标在vue3中,原始类型响应式数据由ref()实现,引用类型响应式数据由reactive()实现。这里我们就来简单实现一下reactive()的原理,来创造一个响应式的对象。

reactive(target): reactive(target)接受一个对象,也可以是函数和数组

function reactive(target) {
    // 创建响应式对象
    return createReactiveObject(target)
}

它返回一个createReactiveObject(target)方法的执行,createReactiveObject(target)来实现一个响应式对象。

创建简单的响应式对象


function isObject(val) {
  return typeof val === 'object' && val !== 'null'
}

function createReactiveObject(target) { // 创建代理后的响应式对象

  if (!isObject(target)) { // 如果不是对象,直接返回
    return target
  }
  
  let baseHandler = {
    get(target,key,receiver) { //receiver:被代理后的对象
      console.log('获取');
      let result = Reflect.get(target,key,receiver)
      return result
    },
    set(target,key,value,receiver) {
      console.log('设置');
      let res = Reflect.set(target,key,value,receiver)
      return res
    },
    deleteProperty(target,key) {
      console.log('删除');
      let res = Reflect.deleteProperty(target,key)
      return res
    }
  }
  //proxy接收两个参数,
  //第一个是一个对象,用isObject()判断一下就行。
  //如果是就对它进行代理,如果不是就直接返回。
  //第二个参数是一个对象,用来定义一些代理方法
  //然后将我们的目标对象和定义的baseHandler对象作为参数传入一个定义的Proxy实例中,并返回这个被代理后的对象。
  //这就是我们创建的响应式对象。这也就是一个简单的reactive()的实现。
  let observed = new Proxy(target, baseHandler)
  return observed
}

为什么要使用Reflect()?

set(target,key,value,receiver),set接受四个参数,依次为目标对象、属性名、属性值和 Proxy代理后的对象,其中最后一个参数可选。当我们使用set时,会把这些参数传进来,就是把这个键对应的值设置到proxy代理后的对象中。也就是说,我们应该receiver.set(target,key,value,receiver)。然而这样会报错,因为不能在proxy内部再调用proxy,上面这样相当于new proxy.set(),会报错。于是我们使用Reflect.set().

什么是Reflect()?

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有:
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上
(2) 修改某些Object方法的返回结果,让其变得更合理。
(3) 让Object操作都变成函数行为。
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。

  • 这里就是因为不能调用receiver.set(),于是我们使用Reflect()中的方法,其实作用是一样的。
  • get(),deletePrpperty(),has()…
  • Proxy有十三种属性,都使用了Reflect.

简单的响应式对象可能会出现的问题

1.当取值的时候,如果我们代理的对象,内部还有对象,这个时候我们需要使用递归来多层代理

解决:

get(target,key,receiver) { //receiver:被代理后的对象
      console.log('获取');
//取值的时候判断,是否是一个对象,如果是就再此代理,
//不是就直接返回。
//我们取值是一层一层往下取,如果有嵌套的话,会执行多次get操作。
      let result = Reflect.get(target,key,receiver)
      return isObject(result) ? reactive(result) : result 
      //递归多层代理,相比于vue2的优势是,vue2默认递归,
      //而vue3中,只要不使用就不会递归。
    },

2.当设置的时候,如果老值等于新值,那么就不需要去设置;如果设置的时候源target对象不具备这个属性,我么需要把这个属性内置到我们代理后的对象中。

解决:

   set(target,key,value,receiver) {
   //老值等于新值,我们就没有去执行视图更新的操作,直接进行了set操作,如果不相等,才去做修改
      // console.log('设置');
      let hadkey = target.hasOwnProperty(key)
      let oldValue = target[key]
      if(!hadkey) {
        console.log('新增');
      } else if (oldValue !== value) {
        console.log('修改');
      }
      //如果对象target内部,压根就没有这个属性,我们去设置这个属性的时候,也让它支持这样的操作,去更新视图。
      let res = Reflect.set(target,key,value,receiver)
      return res
    },

3.如果一个对象已经被代理过一次了,那么我们就不再需要去重复代理

let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = new WeakSet() // 代理过的对象:原对象

WeakSet的特性,当一个对象被代理后,如果再此代理,它不会让重复的对象去到WeakSet中,就不会让它再次被代理。用Set也行,不过利用WeakSet会被垃圾回收机制自动回收的特性,性能会更好。我们在代理之前,去判断WeakMap是否存在我们代理的对象,如果存在,就直接返回,不再执行代理。

优化后的响应式对象

// vue3响应式原理
// 2.0需要递归,数据改变length属性是无效的,对象不存在的属性是不能被拦截的

let toProxy = new WeakMap() // 原对象:代理过的对象
let toRaw = new WeakSet() // 代理过的对象:原对象


function isObject(val) {
  return typeof val === 'object' && val !== 'null'
}

function reactive(target) {
  // 创建响应式对象
  return createReactiveObject(target)
}

function createReactiveObject(target) { // 创建代理后的响应式对象
  if (!isObject(target)) { // 如果不是对象,直接返回
    return target
  }

  let proxy = toProxy.get(target) // 如果对象已经被代理过了,直接返回
  if(proxy) {
    return proxy
  }

  let baseHandler = {
    get(target,key,receiver) { //receiver:被代理后的对象
      console.log('获取');
      // receiver.get() ==》 new proxy().get 这会报错,
      //也就意味着我们不能直接取到被代理对象上的属性
      // 这时候我们需要用到Reflect,这其实也是一个对象,
      //它只不过也含有一些明显属于对象上的方法,且和proxy上的方法一一对应
//取值的时候判断,是否是一个对象,如果是就再此代理,
//不是就直接返回。
//我们取值是一层一层往下取,如果有嵌套的话,会执行多次get操作。
      let result = Reflect.get(target,key,receiver)
      return isObject(result) ? reactive(result) : result 
      //递归多层代理,相比于vue2的优势是,vue2默认递归,
      //而vue3中,只要不使用就不会递归。
    },
    set(target,key,value,receiver) {
      // console.log('设置');
      let hadkey = target.hasOwnProperty(key)
      let oldValue = target[key]
      if(!hadkey) {
        console.log('新增');
      } else if (oldValue !== value) {
        console.log('修改');
      }
      let res = Reflect.set(target,key,value,receiver)
      return res
    },
    deleteProperty(target,key) {
      console.log('删除');
      let res = Reflect.deleteProperty(target,key)
      return res
    }
  }
  let observed = new Proxy(target, baseHandler)
  toProxy.set(target, observed)
  toRaw.add(observed,target)
  return observed
}



let proxy = reactive({'name': 'wn'})
proxy.sex = 'boy'
console.log(proxy.sex);
// proxy.name
// proxy.name = 'kite'
// delete proxy.name
// proxy.age
// proxy.name = 'kite'
// console.log(proxy.name);
// let proxy = reactive([1,2,3])
// proxy.push(4)
// proxy.length = 5
// console.log(proxy);

// 如果一个对象被代理后了,那么就不再需要再被代理

Vue2、Vue3响应式原理的区别

区别1:

vue3可以把不存在的属性添加到对象中,并且会被proxy的get拦截到,它把不存在的属性,赋值到代理后的对象中,值为undefined.而vue2不会,虽然Object.defineProperty也有这个功能。vue2是把所有已经存在的属性进行了一次遍历和递归。再去拦截。而vue3使用的Proxy,不会把所有的属性进行一次遍历,他只是在需要使用到某个属性的时候才去代理。当然它也需要用到递归。但是vue2,vue3的递归是不一样的,vue2,需要把对象的所有属性,进行递归,vue3是一种按需递归。

区别2:

vue2对数组的操作需要重写数组的方法进行重写,而vue3则可以轻松实现。

总结

  • vue2使用Object.defineProperty()实现响应式原理,而vue3使用Proxy()实现。
  • 虽然vue2,vue3面对对象嵌套,都需要递归,但vue2是对对象的所有属性进行递归,vue3是按需递归,如果没有使用到内部对象的属性,就不需要递归,性能更好。
  • vue2中,对象不存在的属性是不能被拦截的。而vue3可以。
  • vue2对数组的实现是重写数组的所有方法,并改变,vue2中,数组的原型来实现,而Proxy则可以轻松实现。而且vue2中改变数组的长度是无效的,无法做到响应式,但vue3可以。

你可能感兴趣的:(javascript,原型模式,开发语言)