vue3源码阅读与实现: 响应式系统-ref模块
ref模块
模块总览

ref函数既可以创建基本数据类型的响应式数据,也可以创建复杂类型的响应式数据.可能有以下这些疑问:
- ref是如何分别处理简单数据类型和复杂数据类型的?
- ref的对简单数据类型的响应式是基于什么实现的?proxy吗?
- 为什么ref构建的响应式数据需要通过.value访问?
让我们带着问题来查看vue是如何实现ref函数的
debugger
通过debugger查看ref的源码,同样的有以下三个关注点:
- ref创建响应式数据
- 访问响应式数据
- 设置响应式数据
重点关注在这三个点,vue做了哪些事情
处理复杂数据类型
使用以下测试用例:
<!doctype html>
<html>
<head>
...
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// 测试ref处理复杂数据类型
const { ref, effect } = Myvue;
debugger
const obj = ref({ name: "响应式" }); // 关注点1: 创建响应式主句
debugger;
effect(() => {
document.querySelector("#app").innerHTML = obj.value.name; // 关注点2:访问响应式数据
});
debugger;
setTimeout(() => {
obj.value.name = "修改之后值"; // 关注点3:设置响应式数据
}, 2000);
...
</html>
关注点1:创建ref
先来进入第一个debugger:

-
点击
setp next into function,进入ref函数,直接调用createRef(),进入这个createRef函数

-
在
createRef函数中,通过RefImpl创建了ref实例

-
进入
RefImpl类中,在这里判断是深层响应式处理时,会调用toReactive函数,进而使用reactive对数据进行响应式处理


- 最后将这个由
reactive函数创建的响应式数据存放在了RefImpl实例的_value属性中
到这里ref创建复杂数据类型响应式的主要逻辑就结束了,由于reactive函数在上一节中已经描述并实现过了这里就不再赘述,
总结
ref处理复杂类型响应式数据主要做了1件事情:
- 判断数据类型为复杂数据类型时,使用
reactive进行数据响应式处理
关注点2:ref的依赖收集
接着进入第二个debugger,看看访问响应式数据时,vue时怎么处理的:

-
首先会触发
effect函数,这个函数在上一节中已经讲过了,主要用来创建ReactiveEffect实例,并赋值给activeEffect,用来保存effect的参数fn,也就是所谓的依赖, -
之后
obj.value触发了.value的get函数,因此会进入RefImpl的get函数中,在这个函数里,会调用trackRefValue进行依赖收集

-
进入
trackRefValue,判断当前activeEffect是否有值,有值则表示需要进行依赖收集,于是通过trackEffects将依赖收集并保存在实例属性dep中,trackEffects函数在上一节已经实现过了,这里不再重复查看

- 最后
get value()方法返回对应的值,此时代码还没有执行完毕,因为此时只访问了value,还没访问到name,然后obj.value.name,就触发了.name的get函数,这个get是由createGetter函数创建的,在上一节中详细讲过
到此访问ref响应式数据的流程全部结束了
总结
ref创建的复杂数据类型响应式数据被访问时,会做两件事情:
- 触发
get value(),收集依赖,这里收集的依赖,对于复杂类型的数据暂时是没用的,ref中复杂类型都交给reactive处理了,其依赖会被保存在targetMap中 - 触发
reactive创建的proxy的get,进行依赖收集工作
关注点3:ref的依赖触发
在这里,我们通过obj.value.name重新设置了响应式数据的值,同样的这里.value会先触发get value()
-
在
get value()中调用trackRefValue,判断activeEffect是undefined,因此什么都不做返回this._value,然后.name会触发set,接下来就和reactive创建的响应式数据一样了,去触发依赖

总结
到这里我们可以知道,对于响应式数据,ref实际是交给reactive处理的.
处理简单数据类型
将上面的测试用例进行简单修改:
<!doctype html>
<html>
<head>
...
<script src="../../dist/vue.js"></script>
</head>
<body>
<div id="app"></div>
<script>
// 测试ref处理复杂数据类型
const { ref, effect } = Myvue;
debugger
const obj = ref("响应式"); // 关注点1: 创建响应式主句
debugger;
effect(() => {
document.querySelector("#app").innerHTML = obj.value; // 关注点2:访问响应式数据
});
debugger;
setTimeout(() => {
obj.value = "修改之后值"; // 关注点3:设置响应式数据
}, 2000);
</script>
</body>
</html>
同样,从三个关注点入手:
ref创建响应式数据- 访问响应式数据
- 设置响应式数据
重点关注在这三个点,vue做了哪些事情
关注点1:创建ref
进入第一个debugger,和处理复杂数据类型相似,会创建RefImpl实例,但不会使用toReactive处理数据,而是直接保存到实例的_value属性上

总结
ref在创建简单数据类型响应式数据时,直接将值保存在_value属性中
关注点2:ref的依赖收集
进入第二个debugger,这里访问了响应式数据的值

-
首先触发effect函数,为activeEffect赋值,然后通过
data.value触发了RefImpl实例的get value()

-
接下来和处理复杂数据类型时相同,通过
trackRefValue收集依赖 -
执行后,
RefImpl实例中的dep属性将保存着所有依赖
总结
访问ref响应式数据时,主要做了一件事情:
- 触发
get value()进行依赖收集,并保存在实例的dep属性中
关注点3:ref的依赖触发
进入第三个debugger,这里设置了响应式数据的值,因此会触发set value(),

-
在
set value()函数中,判断值是否改变,如果改变,触发triggerRefValue,触发收集的依赖

-
在
triggerRefValue中,将收集的依赖dep,传递给triggerEffects(在分析reactive模块时讲解过),统一进行触发

总结
设置ref属性时,vue主要做了1件事情:
- 触发
dep中收集的所有依赖
总结
整个流程走完了,对其中重要的点总结以下:
重要的变量/类:
RefImpl:ref函数返回的便是该类的实例,其中get value和set value是实现简单数据类型响应式的关键,dep属性: 简单数据类型的所有依赖都存放在了实例的dep属性中,而复杂数据类型的依赖都存放在全局变量targetMap中
整体流程:
- 创建响应式数据时,首先会创建
RefImpl实例 - 如果是复杂数据类型,会交给
reactive进行处理 - 如果是简单数据类型,则将值保存在实例的
_value属性上,将依赖保存在dep属性上, - 通过属性访问器,来模拟
proxy的get和set,在get value()中收集依赖,在set value()中触发依赖
实现ref模块
ref函数
主要用来创建ref响应式数据
在packages/reactivity/src/ref.ts中:
import { isRef } from "@vue/shared";
/**
* @message: 常见深层响应式数据
*/
export function ref(value?: unknown) {
// 创建深层响应式数据
return createRef(value, false);
}
/**
* @message: 创建RefImpl实例,shallow是否创建浅层响应式数据
*/
function createRef<T = any>(rawValue: any, shallow: boolean): RefImpl<T> {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
RefImpl类
每个ref响应式数据都是RefImpl的实例
在packages/reactivity/src/ref.ts中:
import { hasChange, isRef } from "@vue/shared";
import { createDep, Dep } from "./deps";
import { toReactive } from "./reactive";
import { activeEffect, trackEffets, triggerEffects } from "./effect";
/**
* @message: ref类,用来生成ref数据实例
*/
class RefImpl<T> {
public _rawValue: T;
private _value: T;
public dep?: Dep = undefined; // 存放该实例的依赖
public readonly __v_isRef = true; // 标志位,用来判断数据是否为ref实例
constructor(
rawValue: T,
public readonly __v_isShallow: boolean
) {
this._rawValue = rawValue;
this._value = __v_isShallow ? rawValue : toReactive(rawValue);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newValue) {
// 值有变化时,设置新值并触发依赖
if (hasChange(this._value, newValue)) {
this._rawValue = newValue;
this._value = toReactive(newValue);
triggerRefValue(this);
}
}
}
/**
* @message: 收集依赖
*/
function trackRefValue(ref) {
if (activeEffect) {
if (!ref.dep) {
ref.dep = createDep();
}
trackEffets(ref.dep);
}
}
/**
* @message: 触发收集的依赖
*/
function triggerRefValue(ref) {
triggerEffects(ref.dep);
}
toReactive函数
在packages/reactivity/src/reactive.ts中:
/**
* @message: 如果是object类型的数据,直接使用reactive创建响应式数据
*/
export function toReactive(value: any) {
return isObject(value) ? reactive(value) : value;
}
新增工具函数
在packages/shared/src/index.ts中:
/**
* @message: 是否是ref创建的响应式数据
*/
export const isRef = (r: any): boolean => {
return !!(r && r._v_isRef === true);
};
/**
* @message: 检测两个值是否有差异
*/
export const hasChange = (v1: any, v2: any) => {
return !Object.is(v1, v2);
};
总结
看完代码,就会发现刚开始的问题都有了答案:
-
ref是如何分别处理简单数据类型和复杂数据类型的?ref函数根据数据类型的不同使用处理方式- 复杂数据类型: 交给
reactive函数处理 - 简单数据类型: 生成
RefImpl实例,在实例中保存值和依赖
- 复杂数据类型: 交给
-
ref的对简单数据类型的响应式是基于什么实现的?proxy吗?简单数据类型的响应式并不是使用
proxy实现的,而是通过js的属性访问器get(),set()来实现依赖收集和依赖触发的 -
为什么
ref构建的响应式数据需要通过.value访问?因为在实现简单数据类型的响应式中,定义了名为
value的属性访问器,所以,访问数据时,必须通过.value才可以触发属性访问器,从而完成依赖收集和触发
转载自:https://juejin.cn/post/7396248017022025791