reactive函数

承上启下

在上一节 ref() 函数中,我们大致理解了 ref() 函数的作用是用来将数据转化为响应式的。但是对于基本类型和引用类型,Vue3底层做的转换不一致:对于基本类型,Vue3 通过 ref() 函数将变量转化为了 RefImpl引用对象,通过 Object.defineProperty() 的 get 与 set 来实现响应式(数据劫持)。对于引用类型,Vue3 则采用基于 ES6的Proxy 的 reactive 函数实现响应式(包含深层响应)。

reactive 定义一个基础类型的响应式数据

 定义一个对象内类型的响应式数据( 基本类型只能使用 ref() 函数转化为响应式 ),我们可以用 reactive 定义一个基本类型的值来试试

<template>
  <p>姓名:{{ a }}</p>
  <button @click="change">点击修改</button>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: "App",
 
  setup() {
    let a = reactive(666)

    console.log(a)

    function change() {
      a = 1234
    }

    return {
      a,
      change
    };
  },
};
</script>

我们可以看到,虽然页面上展示了正确数据,但是在控制台上Vue已经报了警告,并不建议我们这样做,此时我们点击按钮改变数据,发现数据已经改变了,但是页面并没有更新,这表示当前属性a,并不是一个响应式数据,这也表示了为什么Vue3 不建议使用reactive 来转化基础数据

reactive 定义一个基础类型的响应式 

 reactive 定义一个对象类型的响应式数据

上面案例表明 reactive 函数无法将基础数据类型转化为响应式数据,那我们现在来试一试 reactive 函数是否能将 引用类型数据转化为响应式。

<template>
  <p>姓名:{{ userInfo.name }}</p>
  <p>年龄:{{ userInfo.age }}</p>
  <p>工作:{{ userInfo.work }}</p>
  <button @click="change">点击修改</button>
</template>

<script>
import { reactive } from 'vue'
export default {
  name: "App",
 
  setup() {
    let userInfo = reactive({
      name: 'al',
      age:'29',
      work:'前端'
    })

    console.log(userInfo,‘userInfo’)

    function change() {
      userInfo.name = "汤圆仔";
      console.log(userInfo);
    }

    return {
      userInfo,
      change
    };
  },
};
</script>

此时页面展示正确,控制台打印当前经过转化为响应式的数据。是一个 Proxy 代理对象

点击按钮修改数据后,页面展示正确,控制台上打印的 Proxy 代理对象中 name 属性值夜变化了

 此时我们注意到,修改数据时,我们并没有像 ref() 函数转化响应式对象时,通过 xxx.value 来修改属性值,而是直接通过 xxx.xxx 进行修改的。

  reactive 定义一个数组类型的响应式数据

<template>
  <p>工作:{{ hobby }}</p>
  <button @click="change">点击修改</button>
</template>

<script>
import { reactive } from "vue";
export default {
  name: "App",

  setup() {

    let hobby = reactive(['抽烟','喝酒','烫头']) 

    console.log(hobby);

    function change() {
      hobby[0] = '学习'
      console.log(hobby,'hobby');
    }

    return {
      hobby,
      change,
    };
  },
};
</script>

控制台上打印 转换过后 hobby 属性,我们发现也是一个 Proxy 代理对象,但还是一个 Array,

我们通过数组下标改变数据,点击按钮之后发现页面上数据真的修改了,在Vue2中这是行不通的:Vue2中不能通过数组下标直接修改数组,不能通过 length属性 直接设置数组长度

但是还是能证明一点,reactive() 函数能将数组数据转化为响应式数据

reactive 定义一个深层嵌套对象类型的响应式数据

<template>
  <p>工作:{{ userInfo.test.a.b.c }}</p>
  <button @click="change">点击修改</button>
</template>

<script>
import { reactive } from "vue";
export default {
  name: "App",

  setup() {
    let userInfo = reactive({
      test: {
        a: {
          b: {
            c: 666
          }
        }
      }
    });

    function change() {
      userInfo.test.a.b.c = 999
      console.log(userInfo,'userInfo');
    }

    return {
      userInfo,
      change,
    };
  },
};
</script>

点击按钮后,数据修改,同时页面同步更新。深层嵌套数据也被转化为 Proxy代理对象

这能证明 reactive() 函数也能将深层嵌套对象转化为响应式数据

Proxy 和 Reflect

首先在 了解 reactive() 函数之前,我们需要先了解 两个 es6  的API,分别是 :Proxy 和 Reflect

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

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的  -- 引用于 MDN -- Reflect

先说Proxy代理对象,Proxy() 函数接收两个参数,分别是:

  1.  target :也就是源对象,或者说需要被代理的对象,可以是任意类型的对象,包括但不限于数组,函数,以及代理对象
  2. handler:一个对象,以函数作为属性,各属性中的函数分别定义了在执行各种操作 代理对象 的行为( 不是操作源对象 ) 。

任何 对于Proxy代理对象的操作,都会同步转发到 target 源对象上。

如果我不传递参数,或只传递一个参数,那么控制台则会报错。

那么我们来看看这个 Proxy 到底做了啥玩意

let person = {name:'al',age:28}    // target 源对象

let p = new Proxy(person,{})    // p 则是代理对象,handler传递了一个空对象,表示使用默认操作

console.log(person);    // 源对象 {name: 'al', age: 28}

console.log(p);    // 代理对象 Proxy(Object) {name: 'al', age: 28}

当我们操作代理对象时,所有操作会同步反射到源对象上

p.name = '汤圆仔'
console.log(person);    // 源对象 {name: '汤圆仔', age: 28}
console.log(p);    // 代理对象 Proxy(Object) {name: '汤圆仔', age: 28}

p.sex = '男'
console.log(person);    // 源对象 {name: '汤圆仔', age: 28, sex: '男'} 
console.log(p);    // 代理对象 Proxy(Object) {name: '汤圆仔', age: 28, sex: '男'} 

delete person.age
console.log(person);    // 源对象 {name: '汤圆仔', sex: '男'} 
console.log(p);    // 代理对象 Proxy(Object) {name: '汤圆仔', sex: '男'} 

到了这一步,其实就已经完成了数据代理了。

那么我们需要在数据改变的时候监听到,然后更新页面,这才是完整的响应式。这时候,我们就需要 handler 对象中的属性方法:handler 对象中自带 get,set、deleteProperty等等一系列操作对象的方法。顾名思义,我们可以通过 handler 对象中的方法名称来大致判断当前方法的作用。

let p = new Proxy(person,{
  get(target,prop){
    console.log(`${prop}属性被访问了`);
  },
  set(target,prop,value){
    console.log(`${prop}属性被重写了/或新增了${prop} 属性`);
  },
  deleteProperty(target,prop){
    console.log(`${prop}属性被删除了`);
  }
})

然后我们再来操作 代理对象 ,看看控制台展示的是啥

p.name  // name属性被访问了
p.name = '汤圆仔' // name属性被重写了
p.sex = '男'  // name属性被重写了
delete p.age; // age属性被删除了

console.log(person);    // {name: 'al', age: 28}  
console.log(p);    // Proxy(Object) {name: 'al', age: 28}

到这一步我们发现,我们对于代理对象的操作,都可以被监听到,如果我们把 console.log() 换成按照 Vue2.x 的思路,进行依赖收集以及分发,那其实就实现了响应式中的--数据监听部分

但是此时我们打印 源对象 person 和 代理对象p 发现其并没有改变。这是因为我们如果自定义了 handler 对象的内部方法之后,他就不会在按照默认方法去映射源对象,而是会按照我们定义的方法去操作。而此时我们只是进行了打印,并没有对源对象进行操作,所以打印出来的源对象和代理对象都没有发生变化。所以,我们需要对于这些方法进一步完善。

let p = new Proxy(person,{
  // 读取属性时调用该方法
  get(target,prop){
    console.log(`${prop}属性被访问了`);
    return target[prop]
  },
  set(target,prop,value){
    console.log(`${prop}属性被重写了`);
    target[prop] = value;
  },
  deleteProperty(target,prop){
    console.log(`${prop}属性被删除了`);
    delete target[prop];
  }
})

此时我们再次重复之前对代理对象p的操作之后,打印源对象 person 和 代理对象p,可以发现,此时的源对象与代理对象都是经过操作变化之后的

p.name  // name属性被访问了
p.name = '汤圆仔' // name属性被重写了
p.sex = '男'  // name属性被重写了
delete p.age; // age属性被删除了

console.log(person);    // {"name": "汤圆仔","sex": "男"}
console.log(p);    // Proxy(Object) {"name": "汤圆仔","sex": "男"}

其实到了这一步,Vue就完成了对于 引用类型数据的响应式转化了(数据代理完成,数据监听也完成)。而且 我们可以发现 Vue3是直接监听整个需要被转化的对象,而不是像Vue2.x中的一样,需要去一个个监听对象中的属性从而达到深层响应。但是,Vue3对此继续做了一个优化,那就是针对于源数据的操作,Vue3使用了更高效且更安全的方式 Reflect 对象。 

接着我们说说 Reflect 对象,这玩意相比于通过对象.属性 或者 对象[属性]的方式到底有什么优点,让 Vue3 来使用这个替代我们最常用的方式

  1. 统一操作方式:Reflect 将对象的标准操作(如获取、设置和删除属性)转换为方法。这使得操作更加一致和规范:
    // 传统方式
    obj.prop = value;
    
    // 使用 Reflect
    Reflect.set(obj, 'prop', value);
    
  2. 减少异常:Reflect 方法返回一个布尔值来表示操作是否成功,而不是抛出异常。这减少了异常处理的需要

    // 通过 Object.defineProperty 像对象内添加重名属性,会导致报错,程序崩溃
    // 错误信息 TypeError: Cannot redefine property: c at Function.defineProperty
    // 错误信息翻译 不能重复定义属性c
    let obj = {a:1,b:2}
    Object.defineProperty(obj, 'c', {
      get(){
        return 3
      }
    })
    
    Object.defineProperty(obj, 'c', {
      get() {
        return 4
      }
    })
    
    
    // 如果想解决这个问题,我们一般会使用 try-catch 来捕获错误,而不是直接抛出错误导致程序崩溃
    try {
      let obj = { a: 1, b: 2 }
      Object.defineProperty(obj, 'c', {
        get() {
          return 3
        }
      })
    
      Object.defineProperty(obj, 'c', {
        get() {
          return 4
        }
      })
    } catch (error) {
      console.log(error);
    }

    但是在使用 Reflect 对象之后,我们可以很轻松的避免这个问题,因为 Reflect 方法会返回一个布尔值来表示是否成功,我们可以根据布尔值来进行判断后续逻辑,而不用去捕获错误

    let obj = { a: 1, b: 2 }
    let set_3 = Reflect.defineProperty(obj, 'c', {
      get() {
        return 3
      }
    })
    
    if(set_3){
      // dosomethings
    }else {
        Object.defineProperty(obj, 'c', {
        get() {
          return 4
        }
      })
    }
    
  3. 更好的与 Proxy 结合:Reflect API 与 Proxy API 紧密结合,提供了与 Proxy handler 方法相对应的默认实现(例如:get、set、deleteProperty等)。这使得创建和管理代理对象变得更加简单和直观:

    const handler = {
      get(target, prop, receiver) {
        // 使用 Reflect 调用默认行为
        return Reflect.get(target, prop, receiver);
      },
      set(target, prop, value, receiver) {
        // 使用 Reflect 调用默认行为
        return Reflect.set(target, prop, value, receiver);
      }
    };
    
    const proxy = new Proxy(target, handler);
    

简单一点说,就是 通过 Proxy 创建了一个代理对象,然后操作代理对象时,通过 Reflect方法 实现了对于源对象的映射。

所以,Vue3 通过 Proxy( 代理 ) 拦截并监听对象中属性的变化,然后通过 Reflect( 反射 ) 对被代理的对象(也就是源对象)属性进行操作,进而完成了数据拦截与数据监听实现了响应式。

 reactive() 函数的响应式原理

在文章开始,我们通过 reactive() 函数转化了一个对象,使其成为响应式数据,然后打印这个变量,我们发现,底层还是通过 Proxy() 方法实现的数据代理,同时看源码也可以发现,源码也是通过 Reflect() 方法实现的代理对象与源对象的反射

import { track, trigger } from './effect'
import { isObject, hasOwn, isSymbol } from '@vue/shared'
import { ReactiveFlags, toRaw, reactive, readonly } from './reactive'

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (key === ReactiveFlags.RAW && receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)) {
      return target
    }

    const res = Reflect.get(target, key, receiver)

    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      return res
    }

    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

function createSetter(shallow = false) {
  return function set(target: object, key: string | symbol, value: any, receiver: object): boolean {
    const oldValue = (target as any)[key]
    const result = Reflect.set(target, key, value, receiver)
    if (target === toRaw(receiver)) {
      if (!shallow && isObject(value)) {
        value = toRaw(value)
      }
      if (oldValue !== value && (oldValue === oldValue || value === value)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(),
  set: createSetter()
}

所以,Vue3的响应式底层原理还是依赖于 Proxy() 方法,实现了数据代理与数据监听,同时通过 Reflect() 方法反射,实现了通过操作代理对象进而操作源对象

总结

作用:定义一个 对象类型的响应式数据( 基本类型还请使用 ref()函数转化 )

语法:let 代理对象 = reactive(源对象)。接收一个对象或数组,返回一个代理对象

深度:reactive() 定义的响应式数据是深层次的,嵌套的对象或数组中的对象都能响应

底层:内部基于 ES6 的 Proxy实现,通过代理对象操作源对象内部数据。

代理对象 Proxy 和 源对象 并不全等,只有代理对象是响应式的,更改原始对象不会触发更新。所以Vue3 推荐只使用 代理对象进行数据操作

且 Proxy 是直接监听的整个对象实现深层响应,而不是像 Vue2 通过循环来监听对象中的每个属性实现深层响应。

不足:

  1.  有限的值类型:它只能用于对象类型 (对象、数组和如 MapSet 这样的集合类型)。它不能持有如 stringnumber 或 boolean 这样的原始类型
  2. 不能替换整个对象:由于 Vue 的响应式跟踪是通过属性访问实现的,因此我们必须始终保持对响应式对象的相同引用,如果替换整个对象,那么初始的响应式关联会丢失
    let state = reactive({ count: 0 })
    
    // 上面的 ({ count: 0 }) 引用将不再被追踪
    // (响应性连接已丢失!)
    state = reactive({ count: 1 })

      3. 对于解构操作不友好:当我们将响应式对象的基础类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接

const state = reactive({ count: 0 })

// 当解构时,count 已经与 state.count 断开连接
let { count } = state
// 不会影响原始的 state
count++

// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count)
  • 19
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值