前言
我们知道 Vue3 中声明响应式是通过 reactive
和 ref
这两个函数,上篇我们分析了 reactive
的实现原理,接下来我们再来看下 ref
是如何实现的。
案例
首先引入 ref
和 effect
两个函数,之后声明 name
响应式数据,接着又执行 effect
函数,该函数传入了一个匿名函数,最后两秒后又修改 name
值。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { ref, effect } = Vue;
const name = ref("jc");
effect(() => {
document.querySelector("#app").innerHTML = name.value;
});
setTimeout(() => {
name.value = "cc";
}, 2000);
</script>
</body>
</html>
ref 实现
ref
函数定义在 packages/reactivity/src/ref.ts
文件下:
export function ref(value?: unknown) {
return createRef(value, false);
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue;
}
return new RefImpl(rawValue, shallow);
}
ref
函数实际执行的是 createRef
方法,而该方法实际是返回了一个 RefImpl
构造函数的实例对象:
class RefImpl<T> {
private _value: T;
private _rawValue: T;
public dep?: Dep = undefined;
public readonly __v_isRef = true;
constructor(value: T, public readonly __v_isShallow: boolean) {
// 记录原始值
this._rawValue = __v_isShallow ? value : toRaw(value);
// 操作值
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
// 依赖收集
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
// 依赖触发
triggerRefValue(this, newVal);
}
}
}
RefImpl
构造函数会接收传入的值,可能是基本类型也可能是复杂类型,通过 _rawValue
记录原始值,用于之后依赖触发时新旧值的比较,我们需关注 this._value = __v_isShallow ? value : toReactive(value)
,toReactive
函数被定义在 packages/reactivity/src/reactive.ts
中:
export const toReactive = <T extends unknown>(value: T): T =>
isObject(value) ? reactive(value) : value;
可以看出,如果传入的数据为对象类型则调用 reactive
方法,这块逻辑可参考 Vue3 源码解析之 reactive ,否则就直接返回当前值,此时 ref
执行完毕,当前 name
被赋值为 jc
:
我们回过来再看下 RefImpl
构造函数中还有 get value()
和 set value()
这两个方法,那它们具体有什么用?举个例子:
// RefImpl 构造函数
class RefImpl {
// 实例的 getter 行为: ref.value
get value() {
return "get value";
}
// 实例的 setter 行为: ref.value = xxx
set value(newVal) {
console.log("set value");
}
}
const newRef = new RefImpl();
console.log(newRef);
看下输出结果:
当我们执行 newRef.value
时会触发 getter
,而修改值时会触发 setter
。这也是为什么我们赋值或者修改 ref
值时,需要加上 .value
。
另外我们还需知道,对于基本类型的数据 ref
是不具备数据监听的,当赋值或修改值时主动触发了 get
和 set
方法。
之后执行 effect
函数(该原理可查看上篇),传入一个匿名函数,接着执行赋值行为触发 get
方法:
get value() {
// 依赖收集
trackRefValue(this)
return this._value
}
get
方法核心 trackRefValue(this)
实际触发了 trackRefValue
方法进行数据的依赖收集,该方法定义在 packages/reactivity/src/effect.ts
文件中:
export function trackRefValue(ref: RefBase<any>) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if (__DEV__) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: "value",
});
} else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!);
}
if (shouldTrack) {
dep.add(activeEffect!);
activeEffect!.deps.push(dep);
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!,
});
}
}
}
这块逻辑同 reactive
,给指定属性绑定对应的 fn
,目的是 dep
对象与 ReactiveEffect
相关联,完成整个依赖收集的过程。之后两秒后进行修改值触发 set
方法:
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
// 新旧值比较
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
// 依赖触发
triggerRefValue(this, newVal)
}
}
set
方法中 triggerRefValue(this, newVal)
进行依赖触发:
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref);
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: "value",
newValue: newVal,
});
} else {
triggerEffects(ref.dep);
}
}
}
triggerRefValue
方法实际执行了 triggerEffects
,该方法定义在packages/reactivity/src/effect.ts
文件中:
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep];
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
上篇 reactive
我们也分析了这块逻辑,最终执行的是每个 effect.run
方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成。
总结
ref
函数本质上做了三件事:一是返回RefImpl
的实例;二是对数据处理,如果当前数据为基本类型,则直接返回;如果为复杂类型,则调用reactive
返回reactive
数据;三是RefImpl
提供get value
和set value
方法,这就是为什么设置ref
值时,需要带上.value
。ref
基本类型的数据不具备数据监听,赋值或修改值都是主动触发get
和set
方法。- 为什么
ref
类型数据,必须要通过.value
访问值呢?
a. 因为ref
需要处理基本数据类型的响应性,但是对于基本类型数据而言,它无法通过proxy
建立代理。
b. 而vue
通过get value()
和set value()
定义了两个属性函数,通过主动触发这两个函数(属性调用)的形式来进行依赖收集和依赖触发。
c. 所以我们必须通过.value
来保证响应性。