本文通过实现readonly
方法,一步步展示重构的流程。
前言
readonly
接受一个对象,返回一个原值的只读代理。
实现 Vue3 中readonly
方法,先来看一下它的使用。
<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
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
方法,
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
方法中,将重复的地方提取出来封装成函数。
function get(target, key) {
let res = Reflect.get(target, key);
track(target, key);
return res;
}
reactive
方法和readonly
方法中get
操作的唯一区别就是是否调用了track
,为了复用get
函数,可以将其封装成一个高阶函数,传入布尔值进行判断。
function createGetter(isReadonly = false) {
return function get(target, key) {
let res = Reflect.get(target, key);
if (!isReadonly) {
track(target, key);
}
return res;
};
}
代码一致性
那为了保证代码的一致性,get
已经抽离,set
也做相应的抽离封装。
function createSetter() {
return function set(target, key, value) {
let res = Reflect.set(target, key, value);
trigger(target, key);
return res;
};
}
重构到这儿,reactive.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
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
中代码修改成:
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
重复,可以将这块逻辑单独封装成一个函数,让代码的语义化更好。
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
都是创建一个函数,可以采用缓存,减少这样不必要的执行。
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
值更新时,断言这个警告函数执行了。
it("warn when call set", () => {
console.warn = jest.fn();
const original = readonly({ foo: 1 });
original.foo = 2;
expect(console.warn).toHaveBeenCalled();
});
实现上很简单,就是在readonly
的set
方法里打印一下告警提示。
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
,测试通过。