🎯 本节目标
reactive 使用 Proxy 拦截对象,但 Proxy 有个致命弱点:它不能拦截基本数据类型(Primitives) ,比如 number, string, boolean。
let a = 1;-> 这是一个值,没法在他身上绑 get/set。
为了解决这个问题,Vue 3 引入了 ref:
-
Ref : 既然基本类型不能拦截,那我就把它包裹在一个对象里 (Class),这个对象有个属性叫
.value。 -
Accessors : 利用类属性的
get和set拦截.value的访问。 -
proxyRefs : 让你在 template 里写
{``{ count }}而不用写{``{ count.value }}的魔法。
第一步:实现基础 ref
我们先实现最简单的功能:把一个数字变成响应式。
1. 编写测试用例 (Red)
新建 src/reactivity/tests/ref.spec.ts。
TypeScript
// src/reactivity/tests/ref.spec.ts
import { effect } from "../effect";
import { ref } from "../ref";
describe("ref", () => {
it("should hold a value", () => {
const a = ref(1);
expect(a.value).toBe(1);
});
it("should be reactive", () => {
const a = ref(1);
let dummy;
let calls = 0;
effect(() => {
calls++;
dummy = a.value;
});
expect(calls).toBe(1);
expect(dummy).toBe(1);
// 修改值,触发更新
a.value = 2;
expect(calls).toBe(2);
expect(dummy).toBe(2);
// 设置同样的值,不应该触发更新
a.value = 2;
expect(calls).toBe(2);
});
});
2. 代码实现 (Green)
新建 src/reactivity/ref.ts。
关键点 :ref 不像 reactive 那样依赖全局的 targetMap 来存储依赖。因为 ref 本身就是一个对象,它只关心 value 这一个值的变化,所以它的依赖(Dep)直接存在自己实例内部的一个 Set 里即可。
为了复用逻辑,我们需要先去 src/reactivity/effect.ts 做一点小改动,把依赖收集和触发的底层逻辑暴露出来。
A. 准备工作:修改 effect.ts
我们需要把 track 和 trigger 里关于"是否收集"和"执行 effect"的逻辑拆分一下(在 Vue 源码中叫 trackEffects 和 triggerEffects)。
打开 src/reactivity/effect.ts,添加这两个辅助函数:
TypeScript
// src/reactivity/effect.ts
// ... 原有的代码 ...
// 新增:判断是否需要收集
export function isTracking() {
return shouldTrack && activeEffect !== undefined;
}
// 新增:直接收集依赖到指定的 dep (Set) 中
export function trackEffects(dep) {
// 看看 dep 里有没有这个 effect,没有就加进去
if (dep.has(activeEffect)) return;
dep.add(activeEffect);
activeEffect.deps.push(dep); // (这一步是反向收集,暂时不用管,为了完整性可以先加上)
}
// 新增:触发指定的 dep (Set) 中的所有 effect
export function triggerEffects(dep) {
for (const effect of dep) {
if (effect.scheduler) {
effect.scheduler();
} else {
effect.run();
}
}
}
(注意:如果你的 effect.ts 还没实现 scheduler,忽略上面的 scheduler 判断即可,直接 run)
B. 实现 RefImpl 类
现在编写 src/reactivity/ref.ts:
TypeScript
// src/reactivity/ref.ts
import { isTracking, trackEffects, triggerEffects } from "./effect";
import { hasChanged } from "../shared"; // 假设你有个工具库,没有的话就在本文件写个对比函数
class RefImpl {
private _value: any;
public dep; // 存放依赖的 Set
private _rawValue: any; // 存原始值,用于对比
constructor(value) {
this._rawValue = value;
this._value = value;
// 既然是 ref,就得有自己的 dep 容器
this.dep = new Set();
}
get value() {
// 收集依赖
trackRefValue(this);
return this._value;
}
set value(newVal) {
// 只有值变了才触发通知 (对比 hasChanged)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = newVal;
triggerEffects(this.dep);
}
}
}
function trackRefValue(ref) {
if (isTracking()) {
trackEffects(ref.dep);
}
}
export function ref(value) {
return new RefImpl(value);
}
补丁: 在 src/shared/index.ts (如果没有就新建) 添加:
TypeScript
export const hasChanged = (val, newVal) => {
return !Object.is(val, newVal);
};
运行测试 npm run test。 🎉 通过!
第二步:Ref 嵌套对象的情况
如果 ref({ count: 1 }),Vue 的处理方式是:把里面的对象自动转换成 reactive。
1. 增加测试用例
在 src/reactivity/tests/ref.spec.ts 添加:
TypeScript
it("should make nested properties reactive", () => {
const a = ref({
count: 1,
});
let dummy;
effect(() => {
dummy = a.value.count;
});
expect(dummy).toBe(1);
// 修改内部对象的属性
a.value.count = 2;
expect(dummy).toBe(2);
});
2. 修改 Ref 实现
我们需要引入 reactive,并在设置值的时候判断是否为对象。
TypeScript
// src/reactivity/ref.ts
import { reactive } from "./reactive";
// ... import others
// 辅助函数:如果是对象就 wrap 成 reactive,否则直接返回
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
// src/shared/index.ts 里补一个 isObject
// export const isObject = (val) => val !== null && typeof val === "object";
class RefImpl {
// ... 属性不变 ...
constructor(value) {
this._rawValue = value;
// 核心修改:初始化时进行转换
this._value = convert(value);
this.dep = new Set();
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
// 核心修改:对比的时候要用 rawValue (原始对象) 对比
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = convert(newVal); // 如果 set 的新值是对象,也要转换
triggerEffects(this.dep);
}
}
}
运行测试。🎉 通过!
第三步:isRef 和 unRef
这两个是常用的工具函数。
-
isRef: 检查变量是否是 ref。 -
unRef: 如果是 ref 返回.value,否则返回本身。(常用于val = unRef(val)防御性编程)。
1. 测试用例
src/reactivity/tests/ref.spec.ts
TypeScript
it("isRef", () => {
const a = ref(1);
const user = reactive({ age: 1 });
expect(isRef(a)).toBe(true);
expect(isRef(1)).toBe(false);
expect(isRef(user)).toBe(false);
});
it("unRef", () => {
const a = ref(1);
expect(unRef(a)).toBe(1);
expect(unRef(1)).toBe(1);
});
2. 代码实现
给 RefImpl 加一个标志位。
TypeScript
// src/reactivity/ref.ts
class RefImpl {
public __v_isRef = true; // 标志位
// ...
}
export function isRef(ref) {
return !!ref.__v_isRef;
}
export function unRef(ref) {
// 是 ref 就拿 value,不是就拿本身
return isRef(ref) ? ref.value : ref;
}
第四步:proxyRefs (魔法时刻 🪄)
这可能是本节最晦涩但最有用的部分。 场景 :在 Vue 的 template 里,你写 {``{ count }} 就可以直接显示 ref 的值,不需要写 {``{ count.value }}。 原理 :Vue 在 setup 返回对象给 template 时,套了一层 proxyRefs。
1. 测试用例
src/reactivity/tests/ref.spec.ts
TypeScript
it("proxyRefs", () => {
const user = {
age: ref(10),
name: "xiaohong",
};
// 创建代理
const proxyUser = proxyRefs(user);
// get: 不需要 .value
expect(user.age.value).toBe(10);
expect(proxyUser.age).toBe(10);
expect(proxyUser.name).toBe("xiaohong");
// set: 不需要 .value
proxyUser.age = 20;
expect(proxyUser.age).toBe(20);
expect(user.age.value).toBe(20);
// set: 如果把 ref 换成普通值,原 ref 应该被替换
proxyUser.age = ref(10);
expect(proxyUser.age).toBe(10);
expect(user.age.value).toBe(10);
});
2. 代码实现
这也是一个 Proxy,核心逻辑是:get 时如果是 ref 就拆包,set 时如果是 ref 就赋值给 .value。
TypeScript
// src/reactivity/ref.ts
export function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, {
get(target, key) {
// 这里的 unRef 帮我们省去了 .value
return unRef(Reflect.get(target, key));
},
set(target, key, value) {
// 这里的逻辑有点绕:
// 如果原来的值是 ref,并且新值不是 ref,那我们要去修改原来那个 ref 的 .value
if (isRef(target[key]) && !isRef(value)) {
return (target[key].value = value);
} else {
return Reflect.set(target, key, value);
}
},
});
}
运行测试。🎉 全部通过!
🧠 核心知识点总结 (Review)
-
Ref vs Reactive:
-
Reactive 是 Proxy,针对对象,自动拦截。
-
Ref 是 Class,针对基本类型(或对象),利用 getter/setter 拦截
.value。 -
Ref 包裹对象时,内部其实还是调用的 Reactive。
-
-
依赖收集差异:
-
Reactive 依赖全局
targetMap(WeakMap)。 -
Ref 依赖实例内部的
this.dep(Set)。
-
-
proxyRefs:
- 这是 Vue 3 在模板中自动脱 ref 的原理,本质上是一个针对 ref 特殊处理的 Proxy。
✅ 你的今日任务
-
修改 effect.ts : 抽离
trackEffects和triggerEffects。 -
实现 Ref : 完成
RefImpl类及其单测。 -
实现 proxyRefs: 完成自动脱 ref 功能。
(Ref 是个盒子,Reactive 是个守卫。proxyRefs 是个自动开箱机)
现在,你已经手握 Vue 3 响应式系统最核心的两把武器了(Ref & Reactive)。 下一节,我们要进入 专题五:computed 计算属性 ,它将把你刚学的 effect 和 ref 完美结合起来,并引入 缓存 和 调度器 的概念。