上篇介绍了vue3创建响应式数据的apireactive
,接下来将介绍同样可以创建响应式数据的apiref
。在介绍ref源码之前先看看reactive和ref有什么区别。
- reactive底层是基于Proxy来实现的,而ref底层是通过Object.defineProperty来实现的。
- ref可以接受基础类型和对象(走的还是reactive的逻辑),而reactive只能接受对象。
- ref取值时会多一层
1、ref
import { trackEffects, triggerEffects } from "./effect";
import { reactive } from "./reactive";
function toReactive(value) { // 判断传入的参数,如果时对象,就走reactive的逻辑,如果是基础类型直接返回值
return isObject(value) ? reactive(value) : value;
}
// ref函数
export function ref(value) {
return new RefImpl(value);
}
class RefImpl{
public _value;
public dep = new Set; // 储存依赖的set集合
public __v_isRef = true; // ref对象的标识
constructor(public rawValue) {
this._value = toReactive(rawValue);
}
get value() {
trackEffects(this.dep); // 收集依赖(对应的effe)
return this._value;
}
set value(newValue) {
if(newValue !== this.rawValue) {
this._value = toReactive(newValue);
this.rawValue = this._value;
triggerEffects(this.dep); // 触发更新(执行对应的函数)
}
}
}
// effect.ts的trackEffects,triggerEffects方法
export function trackEffects(dep) {
if(activeEffect) {
dep.add(activeEffect);
}
}
export function triggerEffects(effects) {
effects = new Set(effects); // 让循环的effects和上一个effects使用不同的引用,防止循环引用造成死循环
effects.forEach(effect => {
// 注意: 这里要避免run触发trigger,要不会造成死循环
if(effect !== activeEffect) {
if(effect.scheduler) {
effect.scheduler(); // 如果用户传入了调度函数,则用用户的
}else {
effect.run(); // 否则默认刷新视图
}
}
})
}
调试代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { ref, effect } = VueReactivity
const personName = ref('张三')
effect(() => {
document.getElementById('app').innerHTML = `名字${personName.value}`
})
setTimeout(() => {
personName.value = '李四'
}, 2000)
</script>
</body>
</html>
以上代码可知ref方法返回一个RefImpl实例。需要注意的是在访问该实例的值,或者改变该实例的值时要加上value属性,因为value是该实例上的一个属性。如上测试代码中personName.value会触发get方法,进行以来收集,与reactive不同的是,收集的依赖是储存在该实例的dep属性中。当执行personName.value = '李四'时,会触发set方法,触发更新。ref创建的响应式数据格式如下:
2、toRef
在介绍源码之前先讲下其功能和具体使用场景。当我们通过reactive创建一个响应式对象时,为了更加方便的去使用该对象的属性值,我们通常会使用解构赋值的方式去拿到该变量,这样我们拿到的值可能就不是一个响应式变量了,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { reactive, effect } = VueReactivity
const personInfo = reactive({
name: '张三',
age: 18,
friendInfo: {
name: '李四',
age: '18'
}
})
const {name, age, friendInfo} = personInfo
console.log(name, age, friendInfo) // name,age已经不是响应式变量了,
</script>
</body>
</html>
打印结果为:
如上图可以name,age已经不是响应式变量了,而friendInfo仍然还是响应式的,这是为什么呢?其实之前没解构之前name,age可以做到响应式是因为personInfo是响应式对象,在personInfo.name,personInfo.age的时候会触发get从而进行依赖收集。而当从personInfo中解构name,age时,解构的两个值,即当然不会是响应式的了。而friendInfo还是响应式是因为在reactive创建响应式对象的时候,如果其某个属性的值是对象的话会进行递归,让其值也变成响应式对象,所以这时解构出的friendInfo也是一个值,只是这个值本身就是个响应式对象。那怎么让name,age也变成响应式的呢?继续看如下代码:
<!--
* @Description:
* @Version: 2.0
* @Autor: 杨
* @Date: 2022-10-11 14:45:45
* @LastEditors: 杨
* @LastEditTime: 2022-10-14 14:56:15
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { reactive, effect, toRef } = VueReactivity
const personInfo = reactive({
name: '张三',
age: 18,
friendInfo: {
name: '李四',
age: '18'
}
})
+ const name = toRef(personInfo, 'name') // 让name变成响应式变量
+ const age = toRef(personInfo, 'age') // // 让age变成响应式变量
console.log(name, age)
effect(() => {
document.getElementById('app').innerHTML = `名字:${name.value} 年龄:${age.value}`
})
setTimeout(() => {
name.value = '王五'
age.value = 19
}, 2000)
</script>
</body>
</html>
接下来看看toRef是怎么做到让name,age变成响应式的。
class ObjectRefImpl{ // 只是将.value属性代理到原始类型上
constructor(public object, public key) {
}
get value() {
return this.object[this.key]; // 触发get方法时会直接访问源响应式对象
}
set value(newValue) {
this.object[this.key] = newValue; // 触发set时也是直接改变源响应式对象的对应属性的值
}
}
export function toRef(object, key) {
return new ObjectRefImpl(object, key);
}
其实toRef做的事很简单,就是在你name.value的时候做了一下劫持,直接去访问源响应式对象的属性,这样就会触发依赖收集,同理给name.value设置值得时候也是直接操作源响应式对象的。
3、toRefs
toRefs其实就是基于toRef来实现的,这里不做过多的赘述。看如下代码:
export function toRefs(object) {
const result = isArray(object) ? new Array(object.length) : {};
for(let key in object) {
result[key] = toRef(object, key);
}
return result;
}
toRefs做的就是基于toRef,一次性将解构出来的变量变成响应式的。用法如下:
<!--
* @Description:
* @Version: 2.0
* @Autor: 杨
* @Date: 2022-10-11 14:45:45
* @LastEditors: 杨
* @LastEditTime: 2022-10-14 15:36:25
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./reactivity.global.js"></script>
<script>
const { reactive, effect, toRef } = VueReactivity
const personInfo = reactive({
name: '张三',
age: 18,
})
// const name = toRef(personInfo, 'name')
// const age = toRef(personInfo, 'age')
const {name, age} = toRefs(personInfo)
console.log(name, age)
effect(() => {
document.getElementById('app').innerHTML = `名字:${name.value} 年龄:${age.value}`
})
setTimeout(() => {
name.value = '王五'
age.value = 19
}, 2000)
</script>
</body>
</html>