VUE3中setup语法糖解决响应式的方案,所有的只要被ref或者reactive包裹的变量,都会转变成响应式。而在VUE2中,要想做成响应式,必须将变量定义在data函数中。
1、ref:将一个属性或者对象定义成ref对象,也就是将一个属性或者对象变成响应式,修改值必须.value
才能处理对应的值。
- 以下代码定义了三个User对象,并且都是使用ref做成了响应式,当点击按钮改变User对象的值的时候,页面上的值也会被改变,这就是响应式的作用。
- 使用ref获取dom元素。
ts
import { ref, onMounted } from 'vue'
import type { Ref } from 'vue' // Ref是一个类型定义,类型定义导入的时候必须使用type关键字
// 定义User的各个属性的字段类型
type UserType = {
name: string
age: number
}
// 三种不同的User对象的定义
const User = ref<UserType>({ name: '小明', age: 12 })
const User1: Ref<UserType> = ref({ name: '小明', age: 12 })
const User2 = ref({ name: '小明', age: 12 })
const refTest = () => {
User.value.age = 18
User1.value.age = 18
User2.value.age = 18
}
// 使用ref获取dom元素
const dom = ref<HTMLElement>()
onMounted(() => {
console.log(dom.value?.innerHTML) // onMounted结束之后,才能获取到dom元素,所以需要放在onMounted中才能获取到dom
})
<button @click="refTest" style="height: 100px; width: 100px; background: green">refTest</button>
<div>
<p>User: {{ User }}</p>
<p>User1: {{ User1 }}</p>
<p>User2: {{ User2 }}</p>
</div>
<div ref="dom">通过ref获取dom</div>
2、isRef:用来判断一个属性或者对象是不是ref对象。
isRef实际上在项目中很少使用,然而在ref源码中很多地方都在使用
ts
import { ref, isRef } from 'vue'
const a = ref<number>(1)
const b = 1
console.log('a是ref对象:', isRef(a))
console.log('b是ref对象:', isRef(b))
a是ref对象: true
b是ref对象: false
3、shallowRef:和ref的作用相似,但是shallowRef只能用来做浅层响应式
shallowRef只能用来做浅层响应式,也就是说他只能做到修改到.value
的这一层,.value
后边的数据他不能响应式的修改。
- 当我们点击shallowRefTest按钮时,UserE2在页面上的输出是没有任何变化的,但是如果我们在控制台上去看UserE2对象的时候会发现,实际上他的值已经改变了,但是不能渲染到页面上。
- 当我们点击shallowRefTest1按钮时,UserE2在页面上的输出发生了变化,这是因为UserE1的处理是正确的,触发了整个ref的机制,导致UserE2的值也被改变了,并正确的渲染到了界面上。
- 当我们点击shallowRefTest2按钮时,UserE2、UserE1在页面上的输出发生了变化,这是因为UserE1的处理是正确的,shallowRef的响应式处理只能从
.value
后边修改。 - 当我们点击shallowRefTest3按钮时,UserE2、UserE1在页面上的输出发生了变化,这是因为ref的改变的会影响shallowRef的改变,也就是说,shallowRef和ref的处理放一起的时候会被ref影响,shallowRef也会变成深响应式,原因是ref底层会调用triggerRef,而triggerRef会强制收集所有的改变,进而导致shallowRef深层次的改变。
ts
import { ref, shallowRef, triggerRef } from 'vue'
const a = ref<number>(1)
const UserE1 = shallowRef({ name: "小明", age: 12 });
const UserE2 = shallowRef({ name: "小明", age: 12 });
const shallowRefTest = () => {
UserE2.value.age = 18;
};
const shallowRefTest1 = () => {
UserE2.value.age = 18;
UserE1.value = {
name: "小明1",
age: 121,
};
};
const shallowRefTest2 = () => {
UserE2.value = 18;
UserE1.value = {
name: "小明1",
age: 121,
};
};
const shallowRefTest3 = () => {
a.value = 10;
UserE2.value = 18;
};
<button @click="shallowRefTest" style="height: 100px; width: 100px; background: green">shallowRefTest</button>
<button @click="shallowRefTest1" style="height: 100px; width: 100px; background: green">shallowRefTest1</button>
<button @click="shallowRefTest2" style="height: 100px; width: 100px; background: green">shallowRefTest2</button>
<button @click="shallowRefTest3" style="height: 100px; width: 100px; background: green">shallowRefTest3</button>
<div>
<p>UserE1: {{ UserE1 }}</p>
<p>UserE2: {{ UserE2 }}</p>
</div>
4、triggerRef:强制收集所有的改变,和shallowRef一起使用,将shallowRef也变成深层次相应,即得到和ref一样的效果,但是ref和shallowRef不要一起使用,因为ref底层会调用triggerRef,会导致shallowRef的值也会被强制更新
- 当我们点击shallowRefTest4按钮时,如果我们不加
triggerRef(UserE2)
,UserE2在页面上的输出不会发生变化,如果我们加triggerRef(UserE2)
,UserE2在页面上的输出会发生变化,shallowRef也会变成深响应式,原因是triggerRef会强制收集所有的改变,进而导致shallowRef深层次的改变。
ts
import { shallowRef, triggerRef } from 'vue'
const UserE2 = shallowRef({ name: "小明", age: 12 });
const shallowRefTest4 = () => {
UserE2.value.age = 18
triggerRef(UserE2) // 主动调用,触发更新操作
}
<button @click="shallowRefTest4" style="height: 100px; width: 100px; background: green">shallowRefTest4</button>
<div>
<p>UserE2: {{ UserE2 }}</p>
</div>
5、customRef:自己实现ref的逻辑,在实现的过程中可以自己增加其他额外的逻辑处理。
customRef允许我们自己实现ref的逻辑,并增加一些额外的处理,它的实现逻辑主要依赖于get和set方法。比如我们在set的时候需要从后台获取的,假设我们一次调用了一百次后台,但是获取的都是同一个值,那这样我们可以在set方法中使用setTimeOut进行防抖处理,避免服务器压力过大。
ts
import { customRef } from 'vue'
const myRefTest = MyRef<string>('myRefTest')
const myRefChange = () => {
myRefTest.value = 'myRefTest:我自己实现了ref'
}
function MyRef<T>(value: T) {
return customRef((track, triggeer) => {
return {
get() {
track();
return value;
},
set(newValue) {
value = newValue;
triggeer();
},
};
});
}
<button @click="myRefChange" style="height: 100px; width: 100px; background: green">myRefChange</button>
<div>
<p>myRefTest: {{ myRefTest }}</p>
</div>
6、reactive:将一个对象变成响应式,修改值必须.属性
即可处理对应的值
- 以下代码定义了一个UserE3对象,并且都是使用reactive做成了响应式,当点击按钮改变UserE3对象的值的时候,页面上的值也会被改变,也达到了响应式的作用。
- reactive定义对象,因为在reactive的定义中,约束了传入的值只能为Object
ts
import { reactive } from 'vue'
// 定义属性约束
type UserType = {
name: string;
age: number;
};
// 定义一个reactive的对象
const UserE3 = reactive<UserType>({ name: "小明", age: 12 });
const shallowRefTest5 = () => {
UserE3.age = 18;
};
<button @click="shallowRefTest5" style="height: 100px; width: 100px; background: green">shallowRefTest5</button>
<div>
<p>UserE3: {{ UserE3 }}</p>
</div>
7、shallowReactive:也是将一个对象变成浅层响应式,即只能处理对象的第二层属性的值,和shallowRef一样的特性。
ts
import { shallowReactive } from 'vue'
const shallowReactiveE4 = shallowReactive<any>({
foot: {
bar: {
name: "bar",
},
},
});
const shallowRefTest8 = () => {
// shallowReactiveE4.foot.bar.name = 22 // 这里实际上不能修改name的值,只能处理对象的第二层属性的值,即foot这一层
shallowReactiveE4.foot = 22; // 这里实际上能修改foot的值
};
<button
@click="shallowRefTest8"
style="height: 100px; width: 100px; background: green"
>
shallowRefTest8
</button>
<div>
<p>shallowReactiveE4: {{ shallowReactiveE4 }}</p>
</div>
8、readonly:将reactive对象变成只读对象,该只读对象不允许再次被赋值等操作,但是该只读对象值依旧受原始对象值的影响。
ts
import { reactive, readonly } from 'vue'
const readonlyUserE3 = readonly(UserE3);
const shallowRefTest7 = () => {
readonlyUserE3.age = 22; // 这里直接对readonlyUserE3操作不会改变readonlyUserE3对象属性
// UserE3.age = 18 // 如果这里改变的是原始对象,readonlyUserE3也会受影响
};
<button
@click="shallowRefTest7"
style="height: 100px; width: 100px; background: green"
>
shallowRefTest7
</button>
<div>
<p>readonlyUserE3: {{ readonlyUserE3 }}</p>
</div>
9、reactive和ref的区别
- reactive只能定义Object类型的值作为响应式,比如:Map,List等,而ref可以将任意值变成响应式,包括对象,字符串,数字等。
- reactive在处理值的时候直接
.属性
即可处理对应的值,而ref需要.value
才能处理 - reactive在异步场景下不能直接赋值,否则会破坏他的proxy代理对象,从而无法变成响应式。解决办法为:
- 将data解构并使用push方法进行添加
- 定义类似于list1的对象,对象里面在放arr数组属性,然后在使用=将data直接复制给list1对象里面的arr数组属性,使用的时候也是list1.arr
ts
let list = reactive<string[]>([]);
let list1 = reactive<{
arr: string[];
}>({
arr: [],
});
const shallowRefTest6 = () => {
setTimeout(() => {
const data = ["data1", "data2", "data3", "data4", "data5"];
// list = data // 直接用等号赋值会发现,list实际上已经有值了,并且在控制太也能看到,但是页面没有渲染,这就说明:=会破坏响应式
list.push(...data); // 解决办法1:就是将data解构并使用push方法进行添加
list1.arr = data // 解决办法2:使用对象,将数组变为对象的一个属性,并直接赋值。
console.log(list);
}, 1000);
};
<button @click="shallowRefTest6" style="height: 100px; width: 100px; background: green">shallowRefTest6</button>
<div>
<ul>
<li v-for="item in list" :key="item">{{ item }}</li>
<li v-for="item in list1.arr" :key="item">list1 + {{ item }}</li>
</ul>
</div>属性的值
10、响应式原理
Object.defineProperty和Proxy是VUE实现响应式的关键,而VUE2的响应式使用的是Object.defineProperty,而VUE3使用的是Proxy。
10.1 VUE2的响应式原理
a、Object.defineProperty解读:
Object.defineProerty是JS内置对象Object的原生的静态方法,主要用来在一个对象上定义或者修改一个属性并返回该对象。defineProerty有三个参数,他的源码定义如下:
js
/**
* 将属性添加到对象,或修改现有属性的属性。
*
* @param o 要操作的对象。
* @param p 要操作的对象的属性
* @param attributes 对这个要操作的对象的这个属性的一些描述,比如该属性是不是可以被遍历等。
*/
defineProperty<T>(o: T, p: PropertyKey, attributes: PropertyDescriptor & ThisType<any>): T;
// PropertyDescriptor的定义如下
interface PropertyDescriptor {
configurable?: boolean; // 是否可以被删除属性或者再次去修改enumerable、writable这些特性,默认为false,即调用delete时无法删除该属性
enumerable?: boolean; // 该属性是不是可以被枚举(使用fo......in或者Object.keys去循环该对象时,该这个属性可否可见,默认再循环时不允许读取该属性),默认为false。
value?: any; // 该属性的值,默认为undefined,当使用set和get方法的时候,该属性不能被使用了,两者冲突
writable?: boolean; // 该属性是不是可以重新赋值。即使用=号给该属性赋值的时候不生效,当使用set和get方法的时候,该属性不能被使用了,两者冲突
get?(): any; // get方法,当获取该属性的值的时候触发这个方法。注意:不要在get中再次对属性进行获取,这样相当于递归调用,会引发栈溢出错误。
set?(v: any): void; // set方法,当给这个属性设置值的时候触发这个方法。注意:不要在get中再次对属性赋值,这样相当于递归调用,会引发栈溢出错误。
}
简单是使用Object.defineProerty来实现一个数据响应式的案例,当使用User.name获取name的值的时候,get方法会被触发,当使用User.name = nValue给name的赋值的时候,set方法会被触发。
js
const User = { name: "zs", age: 19, sex: "男" };
let cache = User.name;
Object.defineProperty(User, "name", {
configurable: true,
enumerable: true,
// value: undefined, // 使用了set和get,该特性不能再使用了,冲突
// writable: true, // 使用了set和get,该特性不能再使用了,冲突
get: function getter() {
console.log("获取name属性的值"); // 当使用User.name获取name的值的时候,get方法会被触发,进而该语句会被打印
// return User.name; // 不能再get方法执行 User.name取值操作,该操作会再次触发get方法,进而形成死循环,引发栈溢出错误。
return cache;
},
set: function setter(nValue) {
console.log("设置name属性的值,新值为:" + nValue + "旧值为:" + cache); // 当使用User.name = nValue给name的赋值的时候,set方法会被触发,进而该语句会被打印
// User.name = nValue; // 不能再set方法执行 User.name = nValue赋值操作,该操作会再次触发set方法,进而形成死循环,引发栈溢出错误。
cache = nValue;
},
});
b、实现VUE2的响应式
UE2的响应式使用的是Object.defineProperty,利用该特性手工实现VUE2的响应式
js
// 模拟VUE对象,需要使用new关键字初始化MyVue对象的实例
class MyVue {
constructor(options) {
this._data = options.data; // 将data对象挂载到MyVue对象的实例上
this._options = options; // 将options对象挂载到MyVue对象的实例上,备用
this.initData(); // 初始化MyVue对象的实例的时候,就去做拦截的实现
}
// 拦截的实现
initData() {
let data = this._data;
let keys = Object.keys(data);
for (const index in keys) { // 循环data中的每一个属性,准备对data中的每一个属性进行拦截,并将data中的每一个属性挂载MyVue对象的实例上
Object.defineProperty(this, keys[index], {
enumerable: true,
configurable: true,
get: function myGetter() {
console.log("获取" + keys[index] + "值 :" + data[keys[index]]); // myVue.name获取name的值的时候或者myVue.age获取age的值的时候,模拟拦截处理,这里做打印
return data[keys[index]]; //在做返回data中该key的值,
},
set: function mySetter(nValue) {
console.log(
"设置" +
keys[index] +
"值, 新值:" +
nValue +
" 旧值:" +
data[keys[index]]
); // myVue.name = 'lisi'设置name的值的时候或者myVue.age = 28设置age的值的时候,模拟拦截处理,这里做打印
data[keys[index]] = nValue; // 将设置的新值保存到data中
document.getElementById("div").innerText = JSON.stringify(data); // 模拟VUE中响应式,同步更新dom的值
},
});
}
// 处理data数据响应式,
observer(data);
}
}
// 将data数据响应式的入口,封装成为一个类调用
class Observer {
constructor(data) {
this.worker(data);
}
worker(data) {
let keys = Object.keys(data);
// 将data上的key做深层次的响应式处理,后边会用到递归处理,试想:data中的某一个key的数据依旧是一个复杂结构的对象
keys.forEach((key) => {
definedReactive(data, key, data[key]);
});
}
}
function observer(data) {
// 如果data是一个基本数据类型,就返回
const type = Object.prototype.toString.call(data); // 这样获取数据的类型相比于typeOf来说更加精确,
if (type !== "[object Object]" && type !== "[object Array]") {
return;
}
new Observer(data);
}
// data数据响应式处理的逻辑,实际上就是将data上的key深层次使用Object.defineProperty进行拦截处理
function definedReactive(target, key, value) {
observer(target[key]);
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log("获取" + key + "值 :" + value);
return value;
},
set: function reactiveSetter(nValue) {
if (value === nValue) {
return;[[readme]]
}
console.log("设置" + key + "值, 新值:" + nValue + " 旧值:" + value);
document.getElementById("div").innerText = JSON.stringify(nValue);
value = nValue;
},
});
}
html
<!DOCTYPE html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Object.defineProperty</Object></title>
</head>
<body>
<div id="div"></div>
<button onclick="change()" style="width: 100px; height: 30px; background: green;">改变值</button>
</body>
<!-- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> -->
<script src="index.js"></script>
<script>
let myVue = new MyVue({
data: {
name: 'zs',
age: 18,
list: [1, 2, 3],
class: {
id: 001,
leave: '一年级',
dep: {
no: 001,
status: '开启'
}
}
}
}); // 模拟new VUE创建MyVue的实例
document.getElementById('div').innerText = myVue.name + "----" + myVue.age;
// 这里通过设置值,模拟VUE双向绑定的操作。界面上的值也会改变。同时保证myVue实例里面的值也会改变
function change() {
alert("改变值?");
myVue.age = 28;
myVue.name = 'lisi';
myVue.class.id = 002;
myVue.class.dep.no = 002;
}
</script>
</html>
10.2、VUE3的响应式原理
a、Proxy解读
Proxy是JavaScript中的一个内置对象,用于生成对象的代理对象,用于对对象的操作进行拦截,例如:查找、删除、增加、修改、执行等。Proxy中接收两个参数
- target: 要代理的对象。
- handler: 要对target进行的操作,通常是target的各种操作,例如:查找、删除、增加、修改、执行等。
- set:增加一个属性或者删除一个属性
- deleteProperty: 删除某一个属性
- get: 获取一个属性的值
ts
const p = new Proxy(target, handler)
/**
* 生成一个对象的代理对象
*
* @param {object} Person 这是一个对象,包含了一些可选的属性,即源数据
* @param {string} handler 这是一个handle,通常是一个对象,或者函数,用于访问目标数据的方法
*/
const p = new Proxy(Person, {
get(target, prop) { // 访问对象上的某一个属性
},
set(target, prop, value) { // 修改对象上的属性的值或者是新增一个属性
},
deleteProperty(target, prop) { // 删除对象上的一个属性
}
});
b、Reflect解读
Reflect是一个内置对象,是不可构造的,它方法都是静态的,通常和Proxy一起联合使用,使用Reflect可以增强代码的可读性,使得代码更具编程式风格。目前,Reflect 具有Object的部分功能,某些情况下可以替换Object。以下列举几个常用的方法:
ts
/**
* 获取一个对象的属性的值
*
* @param {object} target 这是一个对象,包含了一些可选的属性,即源数据
* @param {string} prop 该对象中的某一个属性的名称
* @param {string} receiver 可选
*/
Reflect.get(target, prop, receiver); // 获取值交给Reflect处理
/**
* 设置对象的属性的值
*
* @param {object} target 这是一个对象,包含了一些可选的属性,即源数据
* @param {string} prop 该对象中的某一个属性的名称
* @param {string} value 新值
*/
Reflect.set(target, prop, value); // 获取值交给Reflect处理
/**
* 函数调用
*
* @param {object} target 要调用函数名
* @param {object} thisArguments this对象,可为空
* @param {object} argumentsList 参数,可为空,多个参数是一个数组,无参可以传递任意空对象
*/
Relfect.apply(target,thisArguments,argumentsList)
c、实现VUE3中的响应式
ts
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Proxy</Object></title>
</head>
<body>
<div id="div">
姓名:<h1 id="name"></h1>
年龄:<h1 id="age"></h1>
性别:<h2 id="gender"></h2>
</div>
<button onclick="change()" style="width: 100px; height: 30px; background: green;">改变值</button>
<button onclick="deletePro()" style="width: 100px; height: 30px; background: green;">删除属性</button>
</body>
<script>
const Person = {
name: 'Jonah',
age: 39
}
document.getElementById('name').innerHTML = Person.name
document.getElementById('age').innerHTML = Person.age
/**
* 生成一个对象的代理对象
*
* @param {object} Person 这是一个对象,包含了一些可选的属性,即源数据
* @param {string} handler 这是一个handle,通常是一个对象,或者函数,用于访问目标数据的方法
*/
const p = new Proxy(Person, {
get(target, prop, receiver) {
console.log('get', `获取${target}的${prop}的值`);
return Reflect.get(target, prop, receiver); // 获取值交给Reflect处理
},
set(target, prop, value) {
console.log('set', `修改${target}的${prop}的值或者新增一个${prop}属性`);
document.getElementById(prop).innerHTML = value // 模拟更新dom
return Reflect.set(target, prop, value); // 修改值交给Reflect处理
},
deleteProperty(target, prop) {
console.log('deleteProperty', `删除${target}的${prop}的${prop}属性`);
document.getElementById(prop).style.display = 'none' // 模拟更新dom
return Reflect.deleteProperty(target, prop); // 删除值交给Reflect处理
}
});
const change =
() => {
alert('修改age的值为')
p.age += 1;
console.log('change', p.age);
alert('添加一个gender属性')
p.gender = '女'
}
const deletePro =
() => {
alert('删除gender属性')
delete p.gender
}
</script>
</html>