3、reactive与readonly 的实现

463 阅读10分钟

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可以直接查看官方文档中的描述
总结如下:

  1. 返回一个对象的响应式代理
  2. 返回的对象以及嵌套中的对象都会通过 proxy包裹,因此不等于源对象
  3. 响应式转换是深层的,它会影响所有的嵌套属性。

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实现

基于上面的测试案例,去完善功能需求。

  1. 需要一个 reactive 的方法去生成并返回一个响应式的对象。
  2. 需要一个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操作。
那么就基于 createReactiveObjectget 来稍微改造一下,从而实现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测试.jpg

所以下一步需要把响应式转换改成深层的。
也就是说对象内的嵌套对象,需要进行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测试案例 都已经通过了

reactive测试通过.jpg

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_REACTIVEif (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的三条功能。

  1. 返回一个对象的响应式代理
  2. 返回的对象以及嵌套中的对象都会通过 proxy包裹,因此不等于源对象
  3. 响应式转换是深层的,它会影响所有的嵌套属性。

并且实现了 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,返回一个原值的只读代理。

  1. 只读代理是深层的:对任何嵌套属性的访问都将是只读的。
  2. 不进行赋值操作。

进行测试案例的处理


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的实现:

  1. readonly 的get本身就与 reactive是一样的,不同的是在进行isReadonly判断时,判断依据 isReactive 不一样
  2. 由于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所有的测试结果都已经通过

readonly测试结果.jpg

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);
        }
		...
    }
}

运行测试后,发现所有的测试文件都通过了测试。并没有出现因为功能的变更,导致其他功能出现影响。

总测试结果.jpg

再上面内容中,

  1. 先是实现了reactive, isReactive
  2. 然后基于reactive 的代码和需要实现的readonly功能进行分析,对代码进行了优化处理
  3. 最后基于优化处理后的代码,快速的实现了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;
}