Vue3 瞎搞第一蛋:初步实现`effect`和`reactive`的关系

204 阅读3分钟

Vue3 瞎搞第一蛋:初步实现effectreactive的关系

我们实现reactivity使用TDD的思想,即实现每个功能之前先写一个测试,然后是测试从失败到成功来实现功能.

  1. 我们知道Vue3如何实现响应式的, 使用reactive实现对对象数据的代理,然后通过effect在响应式数据发生变化的时候,依赖响应式数据的函数再次执行
  • 新建reactive.spec.ts

    describe("reactive", () => {
      it("happy path", () => {
        const origin = { foo: "bar" };
        const obj = reactive(origin);
        expect(origin).not.toBe(obj);
        expect(obj.foo).toBe("bar");
      });
    });
    
  • 新建reactive.ts实现reactive来通过上述测试

    export function reactive(target) {
      return new Proxy(target, {
        get(target, key) {
          const res = Reflect.get(target, key, receiver);
          return res;
        },
        set(target, key, value) {
          const res = Reflect.set(target, key, value, receiver);
          return res;
        },
      });
    }
    
  1. 实现effect功能,在effect内部使用响应式数据,当响应式数据发生变化的时候需要触发这个函数再次执行

    • 新建effect.spec.ts文件

      describe("effect", () => {
        it("happy path", () => {
          const obj = reactive({ foo: 1 });
          const fn = jest.fn(() => obj.foo);
          effect(fn);
          expect(fn).toHaveBeenCalledTimes(1);
          obj.foo = 2;
          expect(fn).toHaveBeenCalledTimes(2);
        });
      });
      
    • 新建effect.ts文件来通过上述测试 上述问题我们应该产生一下疑问

      1. effect内部函数怎么和响应式数据obj建立联系?
      2. 当响应式数据obj发生变化的怎么通过他们之间的联系触发这个函数?

      上面两个问题我们需要一个联系container,当我们在effect函数中读取数据的时候会触发get,然后将这个函数放进container中,然后在set的时候将container中的这个函数拿出来执行。

      // reactive.ts
      import { track, trigger } from "./effect";
      
      export function reactive(target) {
        return new Proxy(target, {
          get(target, key) {
            const res = Reflect.get(target, key, receiver);
            // TODO: track
            track(target, key);
            return res;
          },
          set(target, key, value) {
            const res = Reflect.set(target, key, value, receiver);
            // TODO: trigger 触发依赖
            trigger(target, key);
            return res;
          },
        });
      }
      
      const targetMap = new WeakMap();
      let activeEffect;
      
      // 收集依赖
      export function track(target, key) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
          targetMap.set(target, (depsMap = new Map()));
        }
        let deps = depsMap.get(key);
        if (!deps) {
          depsMap.set(key, (deps = new Set()));
        }
        deps.add(activeEffect);
      }
      
      // 触发依赖
      export function trigger(target, key) {
        let depsMap = targetMap.get(target);
        if (!depsMap) return;
        let deps = depsMap.get(key);
        if (!deps) return;
        deps.forEach((fn) => {
          fn();
        });
      }
      
      export function effect(fn) {
        activeEffect = fn;
        fn();
      }
      

以上就是实现了一个暂时简版reactiveeffect一起作用的例子

  1. 技术总结 effect需要和reactive代理过的对象形成一个映射关系?那么怎么形成这个映射关系,我们知道在reactive 中代理的对象,当我们进行取值的时候会触发get函数, 我们赋值新值的时候就会触发set函数。那么我们就可以在get的时候,将这个函数放在一个中,然后在set的时候从这个中拿出来执行. 大体的关系如下:

    1. 如果有一个key
    target
    	- key
    		- effectFn1
    		- effectFn2
    
    1. 如果有多个key
    target
    	- key1
    		- effectKey1Fn1
    		 - effectKey1Fn2
    	- key2
    		- effectKey2Fn1
    		- effectKey2Fn2
    

    我们这个为什么用WeakMap,是因为WeakMapkey是弱引用,不会影响GC过程。即当对应的key所引用的对象不存在时,说明用户不需要他了,这时候垃圾回收器会完成回收任务.