vue3 reactive和watchEffect的简单实现

91 阅读2分钟

主要代码


const targetMap = new Map();
// 当前被激活的effect
let activeEffect!: ReactiveEffect;
// 是否收集依赖开关
let shouldTrack = false;

/**返回是否有激活中的effect */
export const isTracking = () => {
    return shouldTrack && activeEffect.active == true;
}

/**
 * 收集数据依赖
 * @param target 
 * @param key 
 */
const track = (target, key) => {

    if (!isTracking()) return;
    let depsMap: Map<any, any> = targetMap.get(target);

    if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
    }

    let deps: Set<any> = depsMap.get(key);
    if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
    }

    // 已经存在的key没必要在添加追踪
    if (deps.has(key)) return;
    trackEffect(deps);
}

export const trackEffect = (deps) => {
    deps.add(activeEffect);
    activeEffect.deps.add(deps);
}

/**
 * 数据被修改时候的触发数据变动
 * @param target 
 * @param key 
 */
const trigger = (target, key) => {
    let tar: Map<any, any> = targetMap.get(target);
    let deps: Set<any> = tar.get(key);
    triggerEffect(deps)
}

export const triggerEffect = (deps) => {
    for(const effect of deps) {
        if (effect.scheduler) {
            effect.scheduler();
        } else {
            effect.run();
        }
    }
}

export const reactive = (raw) => {

    return new Proxy(raw, {
        get(target, key) {
            const res = Reflect.get(target, key);
            // 收集好、依赖
            track(target, key);
            return res;
        },
        set(target, key, value) {
            const res = Reflect.set(target, key, value);
            // 触发依赖
            trigger(target, key);
            return res;
        }
    })

}

class ReactiveEffect {
    constructor(private fn: Function, _scheduler?: Function) {
        _scheduler && (this.scheduler = _scheduler);
    }
    public scheduler!: Function;
    public onStop!: Function;
    public active = true;
    public deps: Set<Set<any>> = new Set();
    run() {
        if (!this.active) {
            return this.fn();
        }
        shouldTrack = true;
        activeEffect = this;
        const res = this.fn();
        activeEffect = null as any;
        shouldTrack = false;
        return res;
    }
    stop() {
        if (this.active) {
            if (this.onStop) {
                this.onStop();
            }
            for(const dep of this.deps) {
                dep.clear();
            }
            this.deps.clear();
            this.active = false;
        }
    }
}


export const effect = (fn: Function, options: {
    scheduler?: Function,
    onStop?: Function
} = {}) => {

    const effect = new ReactiveEffect(fn);
    Object.assign(effect, options)
    effect.run();
    const runner: any = effect.run.bind(effect);
    runner.effect = effect;
    return runner;
}

export const stop = (runner: Function& {effect: ReactiveEffect}) => {
    runner.effect.stop();
}

对应测试用例

import { effect, stop, reactive } from '../index'

describe('effect', () => {

    it('happy path', () => {

        const user = reactive({
            age: 10
        })

        let nextAge = 0;

        effect(() => {
            nextAge = user.age + 1
        });

        expect(nextAge).toBe(11);

        user.age++;

        expect(nextAge).toBe(12);

    })

    it('should return runner when call effect', () => {

        let foo = 10;

        const runner = effect(() => {
            foo++;
            return 'foo'
        })

        expect(foo).toBe(11);
        const r = runner();
        expect(foo).toBe(12);
        expect(r).toBe('foo');

    })

    it('scheduler', () => {

        let run: any;
        const scheduler = jest.fn(() => {
            run = runner;
        })
        const obj = reactive({ foo: 1 });
        let dummy = 0;
        const runner = effect(() => {
            dummy = obj.foo;
        }, { scheduler })

        expect(scheduler).not.toHaveBeenCalled();
        expect(dummy).toBe(1);
        
        obj.foo++;
        expect(scheduler).toHaveBeenCalledTimes(1);
        expect(dummy).toBe(1);
        run();
        expect(dummy).toBe(2);

    })
    
    it('stop', () => {

        let dumy;
        const obj = reactive({ prop: 1  })

        const runner = effect(() => {
            dumy = obj.prop;
        })
        obj.prop = 2;
        expect(dumy).toBe(2);
        stop(runner);
        obj.prop = 3;
        expect(dumy).toBe(2);

        runner();

        expect(dumy).toBe(3)

    })

    it('onStop', () => {
        let dumy;
        const obj = reactive({ prop: 1  })
        const onStop = jest.fn();

        const runner = effect(() => {
            dumy = obj.prop;
        }, { onStop })

        expect(dumy).toBe(1)
        expect(onStop).not.toHaveBeenCalled();
        stop(runner);
        expect(onStop).toHaveBeenCalledTimes(1);

    })

})

流程

  1. 通过reactive创建可收集依赖的数据集,在effect(vue中改名叫watchEffect)函数中调用数据集,
  2. 调用effec函数,并传入一个function,functin为自定义的内容
  3. effect函数内部会创建一个ReactiveEffect函数,并设置为全局变量,同时将function传递到ReactiveEffect中,并调用function命名为run
  4. 此时自定义的function中调用rreactive创建的数据集,会触发get/set操作,get操作时候,会检查当前是否有全局变量ReactiveEffect,有则创建一个deps将全部变量塞入到deps中 5.当给数据集赋值时候,会触发set操作,set会去自身的deps里寻找是否有ReactiveEffect, 当找到时候调用ReactiveEffect中的run,来重新触发自定义函数的运行