承上启下
在上一节 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() 函数接收两个参数,分别是:
- target :也就是源对象,或者说需要被代理的对象,可以是任意类型的对象,包括但不限于数组,函数,以及代理对象
- 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 来使用这个替代我们最常用的方式
- 统一操作方式:Reflect 将对象的标准操作(如获取、设置和删除属性)转换为方法。这使得操作更加一致和规范:
// 传统方式 obj.prop = value; // 使用 Reflect Reflect.set(obj, 'prop', value);
-
减少异常: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 } }) }
-
更好的与 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 通过循环来监听对象中的每个属性实现深层响应。
不足:
- 有限的值类型:它只能用于对象类型 (对象、数组和如
Map
、Set
这样的集合类型)。它不能持有如string
、number
或boolean
这样的原始类型。 - 不能替换整个对象:由于 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)