mini-vue3【二】:依赖收集 触发依赖

379 阅读5分钟

effect,reactive(依赖收集 触发依赖)

现在,我们开始写第一个测试。可以把之前初始化项目使用的测试文件给删掉 src\reactivity\tests\index.spec.ts

新建文件 src\reactivity\tests\effect.spec.ts

先放入核心测试流程

describe("effect", () => {
  // 核心代码逻辑
  it("happy path", () => {
    const user = reactive({
      age: 10,
    });

    let nextAge;
    // user.age:依赖收集
    // effect一上来会直接调用fn,然后会触发 user.age 的get操作,触发get时进行依赖收集
    effect(() => {
      nextAge = user.age + 1;
    });
    expect(nextAge).toBe(11);

    // 更新:触发依赖
    // 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
    user.age++;
    expect(nextAge).toBe(12);
  });
});

可以看出这个测试代码里面实际上会涉及到两部分内容,一部分是 reactive,一部分是 effect,这里可以把它拆开,分步骤实现。这里体现了任务拆分的思想。

1、reactive

先来编写 reactive 的测试用例:src\reactivity\tests\reactive.spec.ts

describe("reactive", () => {
  it("happy path", () => {
    const original = { foo: 1 };
    const observed = reactive(original);
    // 他俩绝对是不相等的
    expect(observed).not.toBe(original);
    // observed.foo 应该是 original.foo 的值
    expect(observed.foo).toBe(1);
  });
});

测试用例写好了,我们来实现 reactive

在此之前,我们应该先在 tsconfig.json 中修改 lib,"lib": ["DOM", "ES6"]。否则 Proxy 会报错

新建文件 src\reactivity\reactive.ts

export function reactive(raw) {
  // reactive其实就是 Proxy 的代理
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
      
      // TODO 依赖收集
      return res;
    },
    set(target, key, value) {
      const res = Reflect.set(target, key, value);

      // TODO 触发依赖
      return res;
    }
  })
}

reactive 本质上就是通过 Proxy 来做代理,去拦截;这样我们就知道什么时候触发 get 、set了。

我们先不做依赖收集触发依赖

此时,控制台输入命令:yarn test reactive,即可看到 reactive 的测试用例通过了。

2、effect

新建文件 src\reactivity\effect.ts

class ReactiveEffect {
  private _fn: any;
  constructor(fn) {
    this._fn = fn;
  }
  run() {
    this._fn();
  }
}
export function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  // 当调用effect的时候,直接执行内部的fn(封装在run方法中)
  _effect.run();
}
  • effect 函数接收一个 fn,一上来就需要调用一下 fn
    • 抽离出 ReactiveEffect:面向对象的思想,用一个类来表示
  • 直接执行内部的 run 方法
    • run 方法中是之前保存的 fn 函数

此时注释掉 effect 单测中的更新

describe("effect", () => {
  it("happy path", () => {
    const user = reactive({
      age: 10,
    });
    let nextAge;
    effect(() => {
      nextAge = user.age + 1;
    });
    expect(nextAge).toBe(11);

    // 更新:触发依赖
    // 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
    // user.age++;
    // expect(nextAge).toBe(12);
  });
});

再执行测试命令:yarn test。可以看到测试是通过的。通过后我们再把这两行测试代码打开。

3、依赖收集-触发依赖

我们看回 reactive.ts 文件,之前设置了两个 TODO,将其修改为 track、trigger,这两个函数放在 effect.ts

// src/reactivity/reactive.ts
export function 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;
    },
  });
}

(1)依赖收集:track

我们每一个对象中的每一个 key,它需要一个依赖收集的容器。我们需要创建这个容器,将 effect 放入其中。

因为我们的 effect 不能重复,所以我们选择 Set 这个数据结构

// src/reactivity/effect.ts
const targetMap = new Map();
let activeEffect;
export function track(target, key) {
  // target -> key -> dep
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  
  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }
  // effect收集依赖的过程是在run方法执行中
  dep.add(activeEffect);
}

activeEffect:当前正在执行的 effect

  • effect收集依赖的过程是在run方法执行中
  • 先执行 run 方法中用户传入的 fn 函数
  • 触发 reactive 对象的 get
  • 执行 get 中的依赖收集 track
  • 所以此时 activeEffect 应该是当前正在执行的 effect

在run的过程中设置 activeEffect

class ReactiveEffect {
  ...
  run() {
    // 调用 run 的时候表示当前 effect 是正在执行的状态,把它赋值给 activeEffect
    activeEffect = this;
    this._fn();
  }
}

在调用真正的 fn 函数之前给 activeEffect 赋值,因为调用 fn 过程中,就会直接触发相应的 get(依赖收集) 了。

(2)触发依赖:trigger

// src/reactivity/effect.ts
export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  const dep = depsMap.get(key);

  for (const effect of dep) {
    effect.run();
  }
}
  • 基于 targetkey 取出 dep 对象
  • 遍历 dep,其中每个元素都是一个 effect
  • 执行 effectrun 方法

现在我们再执行一下单测:yarn test,可以看到测试通过了。

全部代码如下:

effect.spec.ts

import { effect } from "../effect";
import { reactive } from "../reactive";

describe("effect", () => {
  // 核心代码逻辑
  it("happy path", () => {
    const user = reactive({
      age: 10,
    });

    let nextAge;
    // user.age:依赖收集
    // effect一上来会调用fn,然后会触发 user.age 的get操作,触发get时进行依赖收集
    effect(() => {
      nextAge = user.age + 1;
    });
    expect(nextAge).toBe(11);

    // 更新:触发依赖
    // 触发依赖:user.age触发set操作时,会把所有收集到的fn拿出来调用一下
    user.age++;
    expect(nextAge).toBe(12);
  });
});

reactive.spec.ts

import { reactive } from "../reactive";

describe("reactive", () => {
  it("happy path", () => {
    const original = { foo: 1 };
    const observed = reactive(original);
    expect(observed).not.toBe(original);
    expect(observed.foo).toBe(1);
  });
});

reactive.ts

import { track, trigger } from "./effect";

// reactive其实就是 Proxy 的代理
export function 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;
    },
  });
}

effect.ts

class ReactiveEffect {
  private _fn: any;
  constructor(fn) {
    this._fn = fn;
  }
  run() {
    // 调用 run 的时候表示当前 effect 是正在执行的状态,把它赋值给 activeEffect
    activeEffect = this;
    this._fn();
  }
}

const targetMap = new Map();
let activeEffect;

export function track(target, key) {
  // 依赖不能重复,我们选择 Set 这个数据结构
  // target -> key -> dep

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

  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }

  // effect收集依赖的过程是在run方法执行中
  // 所以是先执行run方法,这里可以保证 activeEffect 已经有值了
  dep.add(activeEffect);
}

export function trigger(target, key) {
  // 基于target和key取出dep,执行effect的run方法(用户传入的fn)

  const depsMap = targetMap.get(target);
  const dep = depsMap.get(key);

  for (const effect of dep) {
    effect.run();
  }
}

export function effect(fn) {
  // 封装,用类进行表示
  const _effect = new ReactiveEffect(fn);

  // 当调用effect的时候,直接执行内部的fn(封装在run方法中)
  _effect.run();
}

源码参考:@cuixiaorui