实现readonly,重构是一步步进行的

本文通过实现readonly方法,一步步展示重构的流程。

前言

readonly接受一个对象,返回一个原值的只读代理。

实现 Vue3 中readonly方法,先来看一下它的使用。

ts 复制代码
<script setup>
import { readonly } from "vue";

let user = {
  name: "wendZzoo",
  age: 18,
  address: {
    province: "jiangsu",
    city: "suzhou",
  },
};
const copyUser = readonly(user);

user.age = 20;
copyUser.age = 18;

user.address.city = "nanjing";
copyUser.address.city = "suzhou";
</script>

<template>
  {{ user }}
  {{ copyUser }}
</template>

readonly是原值的代理,当原值修改时候,readonly包裹的值也会被修改,但是修改readonly的值,控制台会有警告报错,且无法修改。

readonly是个代理

readonly的实现和reactive一样,只是它无法实现更新操作,意味着没有触发依赖,也就相当于不会收集依赖。

先来写一个核心逻辑的单测,新建readonly.spec.ts

ts 复制代码
import { readonly } from "../reactive";

it("happy path", () => {
  const original = { foo: 1, bar: { bar: 2 } };
  const wapper = readonly(original);

  expect(wapper).not.toBe(original);
  expect(wapper.foo).toBe(1);
});

在reactive.ts中导出readonly方法,

ts 复制代码
import { track, trigger } from "./effect";

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

export function readonly(raw) {
  return new Proxy(raw, {
    get: (target, key) => {
      let res = Reflect.get(target, key);
      return res;
    },
    set: (target, key, value) => {
      return true;
    },
  });
}

执行单测yarn test readonly

重构

单测通过说明readonly方法的核心功能,返回一个代理,已经实现了。

但是代码中可以优化的地方有很多,让我们一步步重构,看看代码是怎么变成你不认识的样子。

抽离get函数

在reactive方法和readonly方法中,将重复的地方提取出来封装成函数。

ts 复制代码
function get(target, key) {
  let res = Reflect.get(target, key);
  track(target, key);
  return res;
}

reactive方法和readonly方法中get操作的唯一区别就是是否调用了track,为了复用get函数,可以将其封装成一个高阶函数,传入布尔值进行判断。

ts 复制代码
function createGetter(isReadonly = false) {
  return function get(target, key) {
    let res = Reflect.get(target, key);
    if (!isReadonly) {
      track(target, key);
    }
    return res;
  };
}

代码一致性

那为了保证代码的一致性,get已经抽离,set也做相应的抽离封装。

ts 复制代码
function createSetter() {
  return function set(target, key, value) {
    let res = Reflect.set(target, key, value);
    trigger(target, key);
    return res;
  };
}

重构到这儿,reactive.ts文件变成了如下这样:

ts 复制代码
import { track, trigger } from "./effect";
function createGetter(isReadonly = false) {
  return function get(target, key) {
    let res = Reflect.get(target, key);
    if (!isReadonly) {
      track(target, key);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value) {
    let res = Reflect.set(target, key, value);
    trigger(target, key);
    return res;
  };
}
export function reactive(raw) {
  return new Proxy(raw, {
    get: createGetter(),
    set: createSetter(),
  });
}
export function readonly(raw) {
  return new Proxy(raw, {
    get: createGetter(true),
    set: (target, key, value) => {
      return true;
    },
  });
}

再次执行单测,验证重构是否破坏了原有功能,测试通过说明重构没有问题,继续下一步的重构。

抽离成单独文件

reactive方法和readonly方法中都存在get和set,那可以优化的点就是将这块逻辑抽离。单独新建一个文件baseHandler.ts

ts 复制代码
import { track, trigger } from "./effect";
function createGetter(isReadonly = false) {
  return function get(target, key) {
    let res = Reflect.get(target, key);
    if (!isReadonly) {
      track(target, key);
    }
    return res;
  };
}
function createSetter() {
  return function set(target, key, value) {
    let res = Reflect.set(target, key, value);
    trigger(target, key);
    return res;
  };
}
export const multableHandler = {
  get: createGetter(),
  set: createSetter(),
};
export const readonlyHandler = {
  get: createGetter(true),
  set: (target, key, value) => {
    return true;
  },
};

相应的,原本reactive.ts中代码修改成:

ts 复制代码
import { multableHandler, readonlyHandler } from "./baseHandler";
export function reactive(raw) {
  return new Proxy(raw, multableHandler);
}
export function readonly(raw) {
  return new Proxy(raw, readonlyHandler);
}

这儿发现 new Proxy重复,可以将这块逻辑单独封装成一个函数,让代码的语义化更好。

ts 复制代码
import { multableHandler, readonlyHandler } from "./baseHandler";
export function reactive(raw) {
  return createActiveObject(raw, multableHandler);
}
export function readonly(raw) {
  return createActiveObject(raw, readonlyHandler);
}
function createActiveObject(raw, baseHandler) {
  return new Proxy(raw, baseHandler);
}

缓存

回顾代码,是否还有优化的地方?

baseHandler.ts中,每次get,set都是创建一个函数,可以采用缓存,减少这样不必要的执行。

ts 复制代码
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
export const multableHandler = {
  get,
  set,
};
export const readonlyHandler = {
  get: readonlyGet,
  set: (target, key, value) => {
    return true;
  },
};

再次执行单测,验证重构是否破坏了原有功能。

返回警告报错

单测,jest.fn()模拟一个警告函数,当readonly值更新时,断言这个警告函数执行了。

ts 复制代码
it("warn when call set", () => {
  console.warn = jest.fn();
  const original = readonly({ foo: 1 });
  original.foo = 2;

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

实现上很简单,就是在readonly的set方法里打印一下告警提示。

ts 复制代码
export const readonlyHandler = {
  get: readonlyGet,
  set: (target, key, value) => {
    console.warn(
      `Set operation on key ${key} failed: target is readonly`,
      target
    );
    return true;
  },
};

最后,执行所有测试yarn test,测试通过。

相关推荐
深蓝AI9 小时前
Meta 开源用了八年的设计系统 astryx:连坐两天 GitHub 总榜第一,组件库开始写给 agent 的说明书
前端·人工智能
ruanno_top10 小时前
轮播每 3 秒自己翻页:键盘停不下来,读屏也不知道换了页
vue.js
IT_陈寒10 小时前
Vue的watch把我坑惨了,原来要这样用才靠谱
前端·人工智能·后端
1-1=010 小时前
Vue 3 模板语
前端·javascript·vue.js
用户76103044737110 小时前
【转战智能眼镜开发】ROKID AIUI 小游戏 亚马逊标枪项目实战讲解
前端
风卿10 小时前
看着AI写代码太孤独了,来欣赏星云吧:一个DSH 装饰插件
前端·javascript
牧艺10 小时前
弹层不用再手算坐标了:CSS Anchor Positioning 从原理到可上线
前端·javascript·css
用户老李11 小时前
YOLO 目标检测自学教程 07:YOLOv3——多尺度预测
前端
moshuying11 小时前
ESP32 墨水屏金价牌(上):从点亮到把图片推过去
前端
李剑一11 小时前
别再抬杠,前端岗已经没了!无论前后端,我们最终都要成“全干”工程师
前端·面试