🎯 本节目标
-
实现
readonly:创建一个只读的响应式对象,它不能被修改,且不需要收集依赖(因为根本不会变)。 -
实现工具函数 :
isReactive和isReadonly,用于判断一个对象是什么类型。 -
代码重构 :因为
reactive和readonly极其相似,我们需要把Proxy的handler抽离出来,避免代码冗余。
第一步:实现 readonly
readonly 和 reactive 的区别在于:
-
Set :
readonly不允许修改,修改时应该报警(console.warn)。 -
Get :
readonly依然可以读取,但因为它不会变,所以不需要调用 track 收集依赖。
1. 编写测试用例 (Red)
新建 src/reactivity/tests/readonly.spec.ts:
TypeScript
// src/reactivity/tests/readonly.spec.ts
import { readonly } from "../reactive";
describe("readonly", () => {
it("should make nested values readonly", () => {
const original = { foo: 1, bar: { baz: 2 } };
const wrapped = readonly(original);
// 1. 依然是 Proxy,不等于原对象
expect(wrapped).not.toBe(original);
// 2. 能够读取值
expect(wrapped.foo).toBe(1);
});
it("should call console.warn when set", () => {
// 模拟 console.warn
console.warn = vi.fn();
const user = readonly({
age: 10,
});
// 修改只读属性
user.age = 11;
// 期望 warn 被调用
expect(console.warn).toBeCalled();
});
});
2. 代码重构与实现 (Refactor & Green)
现在 reactive.ts 里的代码会变得臃肿。我们需要把 get 和 set 的逻辑抽离出去。
新建 src/reactivity/baseHandlers.ts。这是一个非常经典的文件结构,Vue 3 源码也是这么拆分的。
A. 编写 baseHandlers.ts
我们需要两个 Handler:
-
mutableHandlers(给 reactive 用) -
readonlyHandlers(给 readonly 用)
这里利用高阶函数(createGetter/createSetter)来生成函数,通过传入参数控制行为。
TypeScript
// src/reactivity/baseHandlers.ts
import { track, trigger } from "./effect";
// 缓存 get 和 set,只创建一次,提高性能
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
// 高阶函数:生成 getter
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
if (!isReadonly) {
// 只有非只读的时候,才需要收集依赖
track(target, key);
}
return res;
};
}
// 高阶函数:生成 setter
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
// 触发更新
trigger(target, key);
return res;
};
}
// 普通响应式对象的 handler
export const mutableHandlers = {
get,
set,
};
// 只读对象的 handler
export const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
// 报警
console.warn(`key: ${String(key)} set 失败,因为 target 是 readonly`, target);
return true;
},
};
B. 修改 reactive.ts
现在 reactive.ts 就变得非常清爽了,只负责创建 Proxy。
TypeScript
// src/reactivity/reactive.ts
import { mutableHandlers, readonlyHandlers } from "./baseHandlers";
export function reactive(raw) {
return createActiveObject(raw, mutableHandlers);
}
export function readonly(raw) {
return createActiveObject(raw, readonlyHandlers);
}
// 内部通用函数:创建 Proxy
function createActiveObject(raw, baseHandlers) {
return new Proxy(raw, baseHandlers);
}
运行测试 npm run test。 🎉 readonly 通过!
第二步:实现 isReactive 与 isReadonly
这是 Vue 面试中的高频题:怎么判断一个对象是被 Proxy 代理过的? 答案是:给它设个陷阱。 我们在 get 里在这个对象上读取一个特殊的 Key(比如 __v_isReactive),如果读取成功了,说明触发了 Proxy 的 get 拦截,那就是响应式的;如果是普通对象,读取这个 Key 会返回 undefined。
1. 编写测试用例 (Red)
更新 src/reactivity/tests/reactive.spec.ts 和 readonly.spec.ts。
src/reactivity/tests/reactive.spec.ts:
TypeScript
import { reactive, isReactive } from "../reactive";
describe("reactive", () => {
it("happy path", () => {
const original = { foo: 1 };
const observed = reactive(original);
// 新增测试
expect(isReactive(observed)).toBe(true);
expect(isReactive(original)).toBe(false);
});
});
src/reactivity/tests/readonly.spec.ts:
TypeScript
import { readonly, isReadonly } from "../reactive";
describe("readonly", () => {
it("should make nested values readonly", () => {
const wrapped = readonly({ foo: 1 });
// 新增测试
expect(isReadonly(wrapped)).toBe(true);
expect(isReadonly({ foo: 1 })).toBe(false);
});
});
2. 实现逻辑 (Green)
A. 修改 baseHandlers.ts
我们需要定义两个枚举字符串(Flags),当用户访问这两个 key 时,直接返回 true。
TypeScript
// src/reactivity/baseHandlers.ts
// 定义枚举以便复用
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly",
}
function createGetter(isReadonly = false) {
return function get(target, key) {
// 拦截特殊 key 的读取
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly;
}
const res = Reflect.get(target, key);
if (!isReadonly) {
track(target, key);
}
return res;
};
}
// ... set 和其他代码保持不变
B. 修改 reactive.ts
导出这两个工具函数。这里的实现技巧非常巧妙:我们尝试去读取那个特殊的属性,如果对象是 Proxy,就会触发 baseHandlers 里的 get,从而返回 true。
TypeScript
// src/reactivity/reactive.ts
import { mutableHandlers, readonlyHandlers, ReactiveFlags } from "./baseHandlers";
export function reactive(raw) {
return createActiveObject(raw, mutableHandlers);
}
export function readonly(raw) {
return createActiveObject(raw, readonlyHandlers);
}
// 检查是否是 Reactive
export function isReactive(value) {
// 如果 value 是普通对象,value["__v_isReactive"] 会是 undefined,转为布尔值就是 false
// 如果 value 是 proxy,会触发 get,返回 true
return !!value[ReactiveFlags.IS_REACTIVE];
}
// 检查是否是 Readonly
export function isReadonly(value) {
return !!value[ReactiveFlags.IS_READONLY];
}
function createActiveObject(raw, baseHandlers) {
return new Proxy(raw, baseHandlers);
}
再次运行 npm run test。 🎉 全部通过!
🧠 核心知识点总结 (Review)
-
代码重构 (Refactoring):
- 通过
createGetter(高阶函数) 传递isReadonly参数,我们用一套逻辑同时处理了reactive和readonly,大大减少了重复代码。
- 通过
-
Flags 模式:
isReactive的实现原理不是在对象上真的加了一个属性,而是通过 拦截 get 。你问我 "你是 Reactive 吗?"(访问__v_isReactive),如果是 Proxy,我就拦截回答 "Yes";如果是普通对象,没人拦截,自然返回undefined。
-
Readonly 的优化:
readonly不会变,所以不需要 track。这是 Vue 3 性能优化的细节之一。
✅ 你的今日任务
-
重构 :将你的代码拆分为
baseHandlers.ts和reactive.ts。 -
实现 :完成
readonly功能及isReactive/isReadonly检查。 -
测试:确保所有测试用例(包括上一节的 effect 测试)全部通过。
下一节,进入 专题四:ref 的实现 。这时候你会面临一个有趣的问题:reactive 只能代理对象,那我想把一个数字 1 变成响应式怎么办?