当前位置: 首页> 房产> 建筑 > reactive函数

reactive函数

时间:2025/8/23 9:22:57来源:https://blog.csdn.net/qq_40792800/article/details/140817882 浏览次数:0次

承上启下

在上一节 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 = 999console.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)
关键字:reactive函数

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: