专题三:完善响应式 —— readonly 与 isReactive

🎯 本节目标

  1. 实现 readonly:创建一个只读的响应式对象,它不能被修改,且不需要收集依赖(因为根本不会变)。

  2. 实现工具函数isReactiveisReadonly,用于判断一个对象是什么类型。

  3. 代码重构 :因为 reactivereadonly 极其相似,我们需要把 Proxyhandler 抽离出来,避免代码冗余。


第一步:实现 readonly

readonlyreactive 的区别在于:

  1. Set : readonly 不允许修改,修改时应该报警(console.warn)。

  2. 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 里的代码会变得臃肿。我们需要把 getset 的逻辑抽离出去。

新建 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.tsreadonly.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)

  1. 代码重构 (Refactoring):

    • 通过 createGetter (高阶函数) 传递 isReadonly 参数,我们用一套逻辑同时处理了 reactivereadonly,大大减少了重复代码。
  2. Flags 模式:

    • isReactive 的实现原理不是在对象上真的加了一个属性,而是通过 拦截 get 。你问我 "你是 Reactive 吗?"(访问 __v_isReactive),如果是 Proxy,我就拦截回答 "Yes";如果是普通对象,没人拦截,自然返回 undefined
  3. Readonly 的优化:

    • readonly 不会变,所以不需要 track。这是 Vue 3 性能优化的细节之一。

✅ 你的今日任务

  1. 重构 :将你的代码拆分为 baseHandlers.tsreactive.ts

  2. 实现 :完成 readonly 功能及 isReactive/isReadonly 检查。

  3. 测试:确保所有测试用例(包括上一节的 effect 测试)全部通过。

下一节,进入 专题四:ref 的实现 。这时候你会面临一个有趣的问题:reactive 只能代理对象,那我想把一个数字 1 变成响应式怎么办?

相关推荐
神色自若10 小时前
vue3 带tabs的后台管理系统,切换tab标签后,共用界面带参数缓存界面状态
前端·vue3
前端小L1 天前
专题一:搭建测试驱动环境 (TypeScript + Vitest)
前端·javascript·typescript·源码·vue3
前端小L1 天前
专题二:核心机制 —— reactive 与 effect
javascript·源码·vue3
幽络源小助理2 天前
知宇发卡系统二开API代销系统开源版 – 支持代理审核与多商家对接
php·源码
luoluoal3 天前
基于python的语音和背景音乐分离算法及系统(源码+文档)
python·mysql·django·毕业设计·源码
luoluoal3 天前
基于python的英汉电子词典软件(源码+文档)
python·mysql·django·毕业设计·源码
Cherry的跨界思维3 天前
【AI测试全栈:Vue核心】19、Vue3+ECharts实战:构建AI测试可视化仪表盘全攻略
前端·人工智能·python·echarts·vue3·ai全栈·ai测试全栈
luoluoal3 天前
基于python的旅游景点方面级别情感分析语料库与模型(源码+文档)
python·mysql·django·毕业设计·源码
源码宝3 天前
SaaS诊所管理信息系统源码,云门诊系统源码,分布式前后端分离+Java+Vue2.0+SpringBoot+MySQL
java·源码·电子病历·电子处方·药品管理·门诊系统·诊所系统