3. reactive与readonly 的实现
API的实现基于typescript搭建一个完整的工程,并引入vitest对功能的实现进行验证,并通过vitest确保后续功能新增和修改不会破坏原有的代码功能 。开篇的代码延续上一篇的代码基础,但是会引入说明。
在Vue的Composition API中,
其中的Reactivity: Core部分
有创建并返回 响应式数据的api:reactive、readonly、ref、computed
还有响应式工具函数isReactive、isReadonly、isRef等api
以及 watchEffect、watch 等侦听响应式数据变化的api
以上api 的实现都绕不开 track ( 依赖收集 ),trigger (依赖触发)。
接下来就是 通过实现reactive、readonly去深入的学习vue。
上一篇的回顾
在上一篇中,已经描述了完整的响应式过程,并且处理了不少的代码。如下伪代码所示:
后续基于方法的基础进行 Vue响应式API 的实现
function createReactiveObject(raw: object) {
return new Proxy(raw, {
get(target, key) {
const res = Reflect.get(target, key);
// TODO
track(target, key);
return res;
},
set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
});
}
export class ReactiveEffect<T = any> {
...
constructor(public fn: () => T) { }
run() {
cleanupEffect(this);
activeEffect = this;
this.fn();
}
}
function cleanupEffect () {}
function trigger () {}
function track () {}
function effect<T = any>(fn: () => T) {
let _effect = new ReactiveEffect(fn);
_effect.run();
}
1 reactive功能分析
对于reactive可以直接查看官方文档中的描述
总结如下:
- 返回一个对象的响应式代理
- 返回的对象以及嵌套中的对象都会通过 proxy包裹,因此不等于源对象
- 响应式转换是深层的,它会影响所有的嵌套属性。
2 reactive测试案例
基于官方的描述,进行测试案例的书写( 从Vue3源码中 copy出部分内容 ), 去实现reactive的核心逻辑
// !# /__tests__/reactive.spec.ts
import { reactive, isReactive } from "../src/reactive";
describe("reactivity/reactive", () => {
test("Object", () => {
// 源对象
const original = { foo: 1 };
// 创建响应式对象
const observed = reactive(original);
// 响应式对象与源对象不相等
expect(observed).not.toBe(original);
// 响应式对象包含源对象的内容
expect("foo" in observed).toBe(true);
expect(Object.keys(observed)).toEqual(["foo"]);
// 响应式数据的初始化值跟源对象的初始状态是一样的
expect(observed.foo).toBe(1);
// 创建响应式后的数据是响应式的
expect(isReactive(observed)).toBe(true);
// 源数据是非响应式的
expect(isReactive(original)).toBe(false);
});
test('nested reactives', () => {
const original = {
nested: {
foo: 1
},
array: [{ bar: 2 }]
};
const observed = reactive(original);
// 通过reactive 响应式转换的内容,响应式的转化是深层次的
expect(isReactive(observed.nested)).toBe(true);
expect(isReactive(observed.array)).toBe(true);
expect(isReactive(observed.array[0])).toBe(true);
});
});
3 reactive实现
基于上面的测试案例,去完善功能需求。
- 需要一个 reactive 的方法去生成并返回一个响应式的对象。
- 需要一个isReactive方法去判断当前对象是否是 reactive对象。
在之前的代码实现中,实现了一个createReactiveObject 的方法,可以返回一个proxy代理后的对象。 现在基于createReactiveObject 去实现一个 reactive 方法。如下所示:
/**
*
* @param target 需要代理的对象
* @returns 返回一个代理对象
*/
export function reactive<T extends object>(target: T): T
export function reactive(target: object) {
return createReactiveObject(target)
}
4 isReactive的分析与实现
基于官网的文字描述进行分析:
isReactive 是检查一个对象是否是由reactive( ) 或shallowReactive( ) 创建的代理。返回值是boolean类型。 isReactive 接受一个对象,并且返回一个boolean值。
如果是经过reactive转换的数据,在读取的时候,就会触发属性的get操作。
反之,如果没有使用过reactive转换过的对象,则不会触发get操作。
那么就基于 createReactiveObject与 get 来稍微改造一下,从而实现isReactive。
// isReactive 接受一个对象作为参数
export function isReactive(value) {
// 双!! 可以将返回的值隐式转化为boolean
return !!value.IS_REACTIVE;
}
function createReactiveObject(raw: object, isReactive = true) {
return new Proxy(raw, {
get(target, key) {
// 读取key值, 如果是调用isReactive,则可以读取到该内容
if( key === "IS_REACTIVE") {
return isReactive;
}
const res = Reflect.get(target, key);
// TODO
track(target, key);
return res;
},
set(target, key, value) { ... }
});
}
5 基于测试完善功能
在完成功能的实现之后,运行测试案例,检测所有的功能都能通过测试案例。
点击Run(Vitest) 运行整个 reactive 的测试。如下图所示,测试并没有完全通过。
测试结果显示 expect(isReactive(observed.array)).toBe(true) 的结果应该为 false, 而不是true。
说明observed.array 并不是一个响应式数据。
即reactive的第三条功能没有实现,响应式转换是深层的,它会影响所有的嵌套属性。
当前的 reactive 并不能深层的进行响应式转化。
所以下一步需要把响应式转换改成深层的。
也就是说对象内的嵌套对象,需要进行reactive转换。
实现的思路是: 在get时,如果当前类型是 object,就对该对象使用 reactive进行转换。
回到get 的代码中, 添加如下代码。
function createReactiveObject(raw: object, isReactive = true) {
return new Proxy(raw, {
get(target, key,receiver) {
...
// 获取当前的对象
const res = Reflect.get(target, key, receiver);
// TODO 依赖收集
track(target, key);
// 新增部分
if(isObject(res)) {
// 如果是对象,并且isReactive,则对该嵌套对象进行一次转换,并返回
return isReactive ? reactive(res) : '';
}
return res;
},
...
});
}
// 判断是否为对象的方法
export const isObject = (val: unknown): boolean => {
return val !== null && typeof val === "object";
};
将功能开发完成之后,去运行测试案例,发现所有的 reactive测试案例 都已经通过了
6 reactive代码优化
在实现reactive的时候,使用到了createReactiveObject方法,在该方法的内部直接固定的写了 一套 get 与 set,并且在其中实现了 isReactive。
但是在后续的代码中,会去实现 readonly、isReadonly、shallowReactive、shallowReadonly等api。
如果每一个api的实现都写一个 createXXX。则会避免不必要的重复代码。
看如下伪代码:
// 创建reactive
function reactive(obj) {
//
return createReactiveObject(obj);
}
function createReactiveObject() {
return new Proxy(...);
}
// 如果要创建一个 readonly
function readonly(obj) {
return createReadonlyObject(obj)
}
function createReadonlyObject() {
return new Proxy(...)
}
上面的代码中,createReactiveObject 和 createReadonlyObject的功能职责都是一样的,
只需要创建并返回代理对象即可。不同的部分只是 proxy 中的get 与 set的内容。
将createReactiveObject进行抽离,并接受参数,按照传入的参数返回proxy。
将代码进行优化处理
// 创建reactive
export const reactiveHandlers = {
get() {...}
set() {...}
};
export const readonlyHandlers = {
get() {...}
set() {...}
};
function reactive(obj) {
// 传入reactive的handler进行创建
return createReactiveObject(obj, reactiveHandlers);
}
function readonly(obj) {
// 传入readonly的handler进行创建
return createReactiveObject(obj,readonlyHandlers)
}
// 按照传递的参数,进行代理对象的返回
function createReactiveObject(target,baseHandlers) {
const proxy = new Proxy(target, baseHandlers);
return proxy;
}
进行完了上述的处理之后,还可以基于reactiveHandlers 对get 和 set 在进行一步细化处理。
// 基于get和set进一步的进行优化处理
const get = createGetter();
const set = createSetter();
export const reactiveHandlers = {
get,
set,
};
//
function createGetter(isReactive = true) {
return function get(target, key, receiver) {
if (key === "IS_REACTIVE") {
return isReactive;
}
const res = Reflect.get(target, key, receiver);
// TODO
track(target, key);
// 新增部分
if (isObject(res)) {
// 如果是对象,并且isReactive,则对该嵌套对象进行一次转换,并返回
return isReactive ? reactive(res) : '';
}
return res;
}
}
function createSetter() {
return function set(target, key, value) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
}
7 isReactive 的优化
// 去读取当前对象的IS_REACTIVE属性,会触发get
export function isReactive(value) {
return !!value.IS_REACTIVE;
}
function createGetter(isReactive = true) {
return function get(target, key, receiver) {
// 如果key是 IS_REACTIVE, 代表进行了isReactive的读取,直接返回当前的 isReactive默认值
if (key === "IS_REACTIVE") {
return isReactive;
}
...
}
}
看上面的代码,如果在后续的开发过程中,由于某些原因,不能使用 IS_REACTIVE 字符串来作为判断依据了。
如果要替换 IS_REACTIVE。就需要修改 !!value.IS_REACTIVE、 if (key === "IS_REACTIVE") 两处地方。
如果使用的地方不止这两处,修改起来会非常的麻烦。
在不影响语义化的前提下,可以快速方便的修改内容,可以将状态进行单独抽离;
如下代码,在保留了 IS_REACTIVE 语义化前提下,如果需要对判断的内容进行修改, 只需要修改ReactiveFlags 对象中的 __v_isReactive 内容即可,尽可能最小化的修改代码。
// 优化后的处理
const ReactiveFlags = {
IS_REACTIVE: "__v_isReactive",
}
export function isReactive(value) {
return !!value[ReactiveFlags.IS_REACTIVE];
}
function createGetter(isReactive = true) {
return function get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return isReactive;
}
...
}
}
8 reactive总结
在上面内容中,一步步的去实现了reactive。 首先基于官网文档的需求描述,然后去书写测试案例(从Vue3源码中copy), 然后基于测试需求,一点点的使用代码去实现reactive的三条功能。
- 返回一个对象的响应式代理
- 返回的对象以及嵌套中的对象都会通过 proxy包裹,因此不等于源对象
- 响应式转换是深层的,它会影响所有的嵌套属性。
并且实现了 isReactive功能,去判断当前对象是否经过 reactive 的转换。
再最后的时候,基于单一职责,对代码进行了优化处理。
最后添加一点点类型,最终代码内容如下:
// !#reactive.ts
import { isObject } from "@wz-vue/shared"
import { reactiveHandlers } from "./baseHandler";
// 元组第一个内容为当前的实现,后续两个为其他api的实现,暂时可忽略
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly",
RAW = "__v_raw",
}
// 类型 IS_READONLY RAW 为后续其他api实现的内容,可暂时忽略
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.RAW]?: any
}
/**
*
* @param target 需要代理的对象
* @returns 返回一个代理对象
*/
export function reactive<T extends object>(target: T): T
// 创建reactive
export function reactive(target: object) {
return createReactiveObject(target, reactiveHandlers)
}
export function isReactive(value: unknown) {
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
function createReactiveObject(raw: object, baseHandlers: ProxyHandler<any>) {
// 创建一个proxy对象
const proxy = new Proxy(raw, baseHandlers);
// 返回一个响应式对象
return proxy;
}
// !#baseHandler.ts
import { track, trigger } from "./effect";
import { reactive, ReactiveFlags } from "./reactive";
import type { Target } from "./reactive"
import { isObject } from "@wz-vue/shared"
const get = createGetter();
function createGetter(isReactive = true) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return isReactive;
}
const res = Reflect.get(target, key, receiver);
// TODO
track(target, key);
// 新增部分
if (isObject(res)) {
// 如果是对象,并且isReactive,则对该嵌套对象进行一次转换,并返回
return isReactive ? reactive(res) : '';
}
return res;
}
}
const set = createSetter();
function createSetter() {
return function set(target: Target, key: string | symbol, value: object) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
}
// 用于创建reactive的handler
export const reactiveHandlers: ProxyHandler<object> = {
// createGetter
get,
// createSetter
set
}
9 readonly测试案例与功能实现
readonly接受一个对象 (不论是响应式还是普通的) 或是一个 ref,返回一个原值的只读代理。
- 只读代理是深层的:对任何嵌套属性的访问都将是只读的。
- 不进行赋值操作。
进行测试案例的处理
import { isReadonly, readonly } from "../src/reactive";
import { vi } from 'vitest'
describe("readonly", () => {
it("should make nested values readonly", () => {
const original = { foo: 1, bar: { baz: 2 } };
const wrapped = readonly(original);
// readonly对象与源对象不相等
expect(wrapped).not.toBe(original);
// 经过readonly转化的,可以使用 isReadonly判断
expect(isReadonly(wrapped)).toBe(true);
expect(isReadonly(original)).toBe(false);
// 深度转化
expect(isReadonly(wrapped.bar)).toBe(true);
expect(isReadonly(original.bar)).toBe(false);
expect(wrapped.foo).toBe(1);
});
it("should call console.warn when set", () => {
console.warn = vi.fn();
const user = readonly({
age: 10,
});
// 赋值时,触发友好的警告提醒
user.age = 11;
expect(console.warn).toHaveBeenCalled();
});
});
基于前面优化后的代码进行功能添加。
使用 createReactiveObject接收 readonlyHandlers 来实现readonly。
使用ReactiveFlags.IS_READONLY 作为判断的依据,实现 isReadonly。
// !# reactive.ts
import { readonlyHandlers } from "./baseHandlers";
export function readonly<T extends object>(target: T): T {
return createReactiveObject(target, readonlyHandlers);
}
export function isReadonly(value: unknown) {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY]);
}
readonlyHandlers的实现:
- readonly 的get本身就与 reactive是一样的,不同的是在进行isReadonly判断时,判断依据 isReactive 不一样
- 由于readonly是只读的,所以他的set是不需要进行trigger,并且最好在使用的时候,进行友好的提示。
基于封装后的代码实现如下:
// !# baseHandlers.TS
const readonlyGet = createGetter(false);
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
console.warn(
`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`,
target
);
return true;
},
}
function createGetter(isReactive = true) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return isReactive;
} else if (key === ReactiveFlags.IS_READONLY) {
return !isReactive
}
...
// 如果是对象,就判断时 reactive | readonly的, 然后进行深度转化
if (isObject(res)) {
return isReactive ? reactive(res) : readonly(res);
}
...
}
}
写完之后,运行查看测试结果是否能通过。
最后发现readonly所有的测试结果都已经通过
10 总结
最后有一点需要进行修改的就是 function createGetter(isReactive = true) 。
因为createReactiveObject 方法本身其语义就是创建响应式对象, 所以默认所有的 createReactiveObject 创建都是响应式数据,只有当出现其他状态, 如 isReadonly 的时候才判断。
修改如下:
将 isReactive 换为isReadonly, 并修改其默认值
运行所有的测试功能,避免因为 readonly 的实现导致 reactive功能出现其他的问题。
const readonlyGet = createGetter(true);
// 修改 createGetter 的默认参数为 isReadonly
// function createGetter(isReactive = true)
function createGetter(isReadonly = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
}
...
// 如果是对象,就判断时 reactive | readonly的, 然后进行深度转化
if (isObject(res)) {
//修改判断
return isReadonly ? readonly(res) : reactive(res);
}
...
}
}
运行测试后,发现所有的测试文件都通过了测试。并没有出现因为功能的变更,导致其他功能出现影响。
再上面内容中,
- 先是实现了reactive, isReactive
- 然后基于reactive 的代码和需要实现的readonly功能进行分析,对代码进行了优化处理
- 最后基于优化处理后的代码,快速的实现了readonly、isReadonly。
最终实现了reactive、readonly、isReactive、isReadonly。并且通过测试,确保逻辑的准确性。 本章所有代码内容如下:
// !# baseHanlders.ts
import { track, trigger } from "./effect";
import { reactive, ReactiveFlags, readonly, isReadonly } from './reactive';
import type { Target } from "./reactive"
import { isObject } from "@wz-vue/shared"
// 创建通用get
const get = createGetter();
const readonlyGet = createGetter(true)
function createGetter(isReadonly = false) {
return function get(target: Target, key: string | symbol, receiver: object) {
if (key === ReactiveFlags.IS_REACTIVE) {
// 触发 isReactive判断
return !isReadonly;
} else if (key === ReactiveFlags.IS_READONLY) {
// 触发 isReadonly判断
return isReadonly
}
// 获取当前的对象
const res = Reflect.get(target, key, receiver);
// TODO 依赖收集
track(target, key);
// 新增部分
if (isObject(res)) {
// 如果是对象,并且isReactive,则对该嵌套对象进行一次转换,并返回
return isReadonly ? readonly(res) : reactive(res);
}
return res;
}
}
const set = createSetter();
// 创建set
function createSetter() {
return function set(target: Target, key: string | symbol, value: object) {
const res = Reflect.set(target, key, value);
trigger(target, key);
return res;
}
}
// reactive的依赖处理对象
export const reactiveHandlers: ProxyHandler<object> = {
get,
set
}
// readonly的依赖处理对象
export const readonlyHandlers: ProxyHandler<object> = {
get: readonlyGet,
set(target, key) {
console.warn(
`key :"${String(key)}" set 失败,因为 target 是 readonly 类型`,
target
);
return true;
},
}
// !#reactive.ts
import { isObject } from "@wz-vue/shared"
import { reactiveHandlers, readonlyHandlers } from "./baseHandler";
export const enum ReactiveFlags {
IS_REACTIVE = "__v_isReactive",
IS_READONLY = "__v_isReadonly",
RAW = "__v_raw",
}
export interface Target {
[ReactiveFlags.IS_REACTIVE]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.RAW]?: any
}
/**
*
* @param target 需要代理的对象
* @returns 返回一个代理对象
*/
export function reactive<T extends object>(target: T): T
export function reactive(target: object) {
return createReactiveObject(target, reactiveHandlers)
}
export function isReactive(value: unknown) {
return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])
}
export function readonly<T extends object>(target: T): T {
return createReactiveObject(target, readonlyHandlers)
}
export function isReadonly(value: unknown) {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
function createReactiveObject(raw: object, baseHandlers: ProxyHandler<any>) {
// 创建一个proxy对象
const proxy = new Proxy(raw, baseHandlers);
// 返回一个响应式对象
return proxy;
}