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