5-实现 readonly

129 阅读3分钟

对应单侧

readonly.spec.ts

import {  reactive, readonly } from "..";

/*
 * @Author: Lin zefan
 * @Date: 2022-03-16 18:03:54
 * @LastEditTime: 2022-03-16 19:26:56
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\reactivity\test\readonly.spec.ts
 *
 */

describe("readonly", () => {
  /** 检测readonly
   * 1. 是否为只读
   * 2. 检测是否为reactive、readonly对象
   */
  it("happy path", () => {
    const original = { foo: 1, bar: { baz: 2 } };
    const state = reactive({
      foo: 1,
    });
    const wrapped = readonly(original);
    wrapped.foo++;
    expect(wrapped.foo).toBe(1);
    expect(wrapped).not.toBe(original);
  });
  
  /** 检测readonly内部调用set
   * 1. 调用Proxy set会输出一个console.warn
   * 2. 校验是否成功输出console.warn
   */
  it("warn then call set", () => {
    console.warn = jest.fn();

    const u = readonly({
      age: 10,
    });
    u.age = 11;

    expect(console.warn).toBeCalled();
  });

核心逻辑

index.ts

/* 看得见的思考
 * 1. readonly 只读,不可以设置
 * 2. 只读那必然不会有收集依赖、设置依赖
 * 3. 那是不是基于reactive的基础上去更改?
**/

import { track, trigger } from "./effect";

export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver);
      trigger(target, key);
      return res;
    },
  });
}

+ export function readonly(raw) {
+   return new Proxy(raw, {
+     get(target, key, receiver) {
+       const res = Reflect.get(target, key, receiver);
+       return res;
+     },
+     set(target, key, value, receiver) {
+       // TODO,设置的时候应该是给一个警告
+       return true;
+     },
+   });
+ }

代码优化

单侧通过!但是,reactive跟readonly看起来有优化空间!改造一下

/* 看得见的思考
 * 1. Proxy内部的get和set接收的是一个函数
 * 2. 可以抽取一个公用的get高阶函数,给一个isReadonly入参来判断是否为readonly
**/

+ function createdGetter(isReadonly = false) {
+   return function (target, key, receiver) {
+     const res = Reflect.get(target, key, receiver);
+     // 如果是readonly,不会进行收集
+     !isReadonly && track(target, key);
+     return res;
+   };
+ }

+ function createdSetter() {
+   return function (target, key, value, receiver) {
+     const res = Reflect.set(target, key, value, receiver);
+     trigger(target, key);
+     return res;
+   };
+ }

export function reactive(raw) {
  return new Proxy(raw, {
+    get: createdGetter(),
+    set: createdSetter(),
  });
}

export function readonly(raw) {
  return new Proxy(raw, {
+    get: createdGetter(true),
    set(target, key, value, receiver) {
      // TODO,设置的时候应该是给一个警告
      return true;
    },
  });
}


告一段落,看起来好多了。但是,好像还有优化的空间?

  1. 提取公用ts
  2. 提取创建P

在 reactivity 目录下新增 baseHandlers.ts

image.png

// baseHandlers.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-16 18:30:25
 * @LastEditTime: 2022-03-17 11:03:43
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\reactivity\baseHandlers.ts
 *
 */
import { track, trigger } from "./effect";

function createdGetter(isReadonly = false) {
  return function (target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
+     // 嵌套转换
+     if (isObject(res)) {
+       return isReadonly ? readonly(res) : reactive(res);
+     }
    // 如果是readonly,不会进行收集
    !isReadonly && track(target, key);
    return res;
  };
}

function createdSetter() {
  return function (target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver);
    trigger(target, key);
    return res;
  };
}

// 避免多次创建,这里直接用变量接收~
const get = createdGetter();
const set = createdSetter();
const readonlyGet = createdGetter(true);

// 可变动的
export const mutableHandles = {
  get,
  set,
};

// 只读的
export const readonlyHandles = {
  get: readonlyGet,
  set(target, key, value, receiver) {
    // 给一个警告
    console.warn(`${key}是只读的,因为被readonly包裹了`, target);
    return true;
  },
};

index.ts 更新

/*
 * @Author: Lin zefan
 * @Date: 2022-03-15 13:08:22
 * @LastEditTime: 2022-03-17 11:03:14
 * @LastEditors: Lin zefan
 * @Description:
 * @FilePath: \mini-vue3\src\reactivity\index.ts
 *
 */
import { mutableHandles, readonlyHandles } from "./baseHandlers";

function createdBaseHandler(raw, baseHandler) {
  return new Proxy(raw, baseHandler);
}

export function reactive(raw) {
  return createdBaseHandler(raw, mutableHandles);
}

export function readonly(raw) {
  return createdBaseHandler(raw, readonlyHandles);
}

shared/index.ts

/*
 * @Author: Lin zefan
 * @Date: 2022-03-15 19:28:09
 * @LastEditTime: 2022-03-17 19:03:20
 * @LastEditors: Lin zefan
 * @Description: 公用hook
 * @FilePath: \mini-vue3\src\shared\index.ts
 *
 */
+ export function isObject(obj) {
+   return obj !== null && typeof obj === "object";
+ }

单侧成功!

image.png