07_实现readonly功能
一、单元测试
describe('readonly', () => {
it('happy path', () => {
const original = { foo: 1, bar: { baz: 2 } };
const wrapped = readonly(original);
expect(wrapped).not.toBe(original);
expect(wrapped.foo).toBe(1);
// ! 不能被set
wrapped.foo = 2;
expect(wrapped.foo).toBe(1);
});
});
可以看出readonly
也是对对象
的一种代理,只不过只能读,不能写。
也就是只能被get
,不能被set
,那也就没必要去track
收集依赖了。
二、代码实现
实现的逻辑也很简单,get
的时候,我们正常返回值。当又set
操作时,我们给予一个警告⚠️。
实现如下:
// src/reactivity/reactive.ts
export function readonly(target) {
return new Proxy(target, {
get(target, key) {
const res = Reflect.get(target, key);
return res;
},
set(target, key, value) {
// todo 抛出警告⚠️ 不可以被set
return true;
},
});
}
单测通过。
三、代码重构
1. 首次重构
当happy path
实现后,此时可以看到reactive
和readonly
的get
比较相似,就可以抽离出一个函数createGetter
,返回一个get
函数。
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
if (!isReadonly) {
track(target, key);
}
return res;
};
}
为了保持代码的一致性,再把set
也抽离出来。
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
};
}
抽离后的代码如下:
import { track, trigger } from './effect';
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
};
}
export function reactive(target) {
return new Proxy(target, {
get: createGetter(),
set: createSetter(),
});
}
export function readonly(target) {
return new Proxy(target, {
get: createGetter(true),
set(target, key, value) {
// ps 此处由于做不一样的操作,就不写成createSetter()了
// todo 抛出警告⚠️ 不可以被set
return true;
},
});
}
抽离完成后,别忘记继续跑一遍全部单测。
2. 二次重构
其实再仔细观察代码,可以看出两个new Proxy
的第二个参数有很多相似的地方,这时候就可以考虑二次封装。
首先进行功能划分,reactive.ts
里面只做对对象的代理。
而具体的代理操作,我们单独抽离出一个文件进行管理,可能后续还有其他类型处理的对象代理,以便延展。
所以,在reactivity
下建立baseHandlers.ts
,专门存储用于代理处理函数。
// + src/reactivity/baseHandlers.ts
import { track, trigger } from './effect';
function createGetter(isReadonly = false) {
return function get(target, key) {
const res = Reflect.get(target, key);
if (!isReadonly) {
track(target, key);
}
return res;
};
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
};
}
// * reactive
export const mutableHandlers = {
get: createGetter(),
set: createSetter(),
};
// * readonly
export const readonlyHandlers = {
get: createGetter(true),
set(target, key, value) {
// todo 抛出警告⚠️ 不可以被set
return true;
},
};
从baseHandlers.ts
中导出mutableHandlers
和readonlyHandlers
,然后在reactive.ts
中进行导入即可。
import { mutableHandlers, readonlyHandlers } from './baseHandlers';
export function reactive(target) {
return new Proxy(target, mutableHandlers);
}
export function readonly(target) {
return new Proxy(target, readonlyHandlers);
}
这时候,reactive.ts
中的代码就很简洁。在这里,我们能看到的是,reactive
和readonly
对对象进行了不同的代理操作。
在大部分情况下,我们只关注这个函数实现了什么功能,这个函数内部具体怎么处理,我们并不关心。
如果一定要看具体的代理逻辑,那就去另一个文件中去阅读。
继续跑一下单测,确保重构没有问题。
3. 三次重构(梅开三度)
reactive
和readonly
的主要逻辑基本相同,都是对象代理,不具备一个良好的语义性。
而后续为了完善现有逻辑,我们应该会在代理操作之前进行一些判断和处理,并不会直接代理数据。
所以为了后续以便于扩展,就继续抽离出进行预处理
和创建响应式对象
的函数createReactiveObject
。如下:
// + src/reactivity/reactive.ts
import { mutableHandlers, readonlyHandlers } from './baseHandlers';
function createReactiveObject(target: any, baseHandlers) {
return new Proxy(target, baseHandlers);
}
export function reactive(target) {
return createReactiveObject(target, mutableHandlers);
}
export function readonly(target) {
return createReactiveObject(target, readonlyHandlers);
}
再看到mutableHandlers
和readonlyHandlers
,会发现,每次调用mutableHandlers
,实际上都会重新创建get
,所以考虑用一个全局变量存储,就不会被销毁。
// + src/reactivity/baseHandlers.ts
const get = createGetter();
const set = createSetter();
const readonlyGet = createGetter(true);
// * reactive
export const mutableHandlers = {
get,
set,
};
// * readonly
export const readonlyHandlers = {
get: readonlyGet,
set(target, key, value) {
// todo 抛出警告⚠️ 不可以被set
return true;
},
};
这样重构就完成了,暂时看不出什么可以再进行优化的点了。
那最后再次跑一下单测。
四、实现警告
到此为止,还剩最后一个地方需要完善,就是上面的todo
,readonly
变量被set
时,抛出警告。
单元测试
先来看一下单测。
tsit('should call console.warn when set', () => { // console.warn() // mock // ps: jest.fn() 用于创建一个 Mock 函数,可以设置该函数的返回值、监听该函数的调用、改变函数的内部实现等等。通过 jest.fn() 创建的函数有一个特殊的 .mock 属性,该属性保存了每一次调用情况 console.warn = jest.fn(); const user = readonly({ age: 10 }); user.age = 11; expect(console.warn).toBeCalled(); });
代码实现
tsexport const readonlyHandlers = { get: readonlyGet, set(target, key, value) { // ! 抛出警告⚠️ 不可以被set console.warn(`key: ${ key } set value: ${ value } failed, because the target is readonly!`, target); return true; } };
最后走一遍单测。
可以看到,警告也打印了,所有的测试用例也都通过了。🍓
ps
这是一个 早起俱乐部!
⭐️ 适合人群:所有想有所改变的人,可以先从早起半小时开始!抽出30分钟,从初心开始!!
⭐️ 没有任何其它意味,只是本人想寻找一起早起、志同道合的小伙伴。