返回

手写Vue3:解密Effect & Reactive的依赖收集与触发机制

前端



前言

这是「手写Vue3」系列的第2篇,前面的系列直达链接如下:

在这一篇,我们将实现Vue3最经典的依赖收集和依赖触发。

增加测试用例

在上一篇中,我们已经搭建好了手写Vue3的测试环境,为了确保手写的代码没有bug,需要编写足够的测试用例,因此,我们在之前编写好的测试用例的基础上增加如下测试用例:

describe('effect', () => {
  it('should trigger effect when reactive data changes', () => {
    let dummy = 0;
    const counter = reactive({ num: 0 });
    effect(() => (dummy = counter.num));
    expect(dummy).toBe(0);
    counter.num++;
    expect(dummy).toBe(1);
  });
});

describe('reactive', () => {
  it('should trigger effects when reactive data changes', () => {
    let dummy = 0;
    const counter = reactive({ num: 0 });
    effect(() => (dummy = counter.num));
    counter.num++;
    expect(dummy).toBe(1);
  });
});

这些测试用例将帮助我们验证依赖收集和依赖触发是否正常工作。

实现Effect & Reactive

1. Effect

Effect是Vue3中用于声明性地监听响应式数据的变化,当响应式数据发生变化时,Effect中的函数就会被重新执行,从而实现视图的更新。

function effect(fn, options = {}) {
  const effect = createReactiveEffect(fn, options);
  effect.run();
  const parentEffect = activeEffect;
  trackEffects(parentEffect);
  return effect;
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    activeEffect = effect;
    cleanup(effect);
    fn();
  };
  effect.options = options;
  return effect;
}

createReactiveEffect函数中,我们创建了一个effect函数,该函数将作为Effect的执行体,并将其作为参数传入trackEffects函数,以便将该Effect添加到响应式数据的依赖列表中。

effect函数中,我们首先将当前的Effect设置为activeEffect,然后调用cleanup函数来清除之前的依赖,最后执行effect函数体。

2. Reactive

Reactive是Vue3中用于创建响应式数据的函数,当响应式数据发生变化时,将触发所有依赖该数据的Effect重新执行。

export function reactive(obj) {
  return createReactive(obj, reactiveHandlers);
}

function createReactive(obj, baseHandlers) {
  return new Proxy(obj, baseHandlers);
}

const reactiveHandlers = {
  get(target, key, receiver) {
    trackEffects(activeEffect, target, key);
    const result = Reflect.get(target, key, receiver);
    return unref(result);
  },
  set(target, key, value, receiver) {
    const oldValue = target[key];
    if (oldValue !== value) {
      target[key] = value;
      triggerEffects(target, key);
    }
    return true;
  },
};

createReactive函数中,我们创建了一个Proxy对象,该对象将作为响应式数据的代理,并将其作为参数传入trackEffects函数,以便将所有依赖该响应式数据的Effect添加到其依赖列表中。

reactiveHandlers中,我们定义了getset两个代理方法,用于拦截对响应式数据的访问和修改操作。在get方法中,我们将当前的Effect添加到响应式数据的依赖列表中,并在获取数据时将其转换为原始值。在set方法中,我们首先判断新值是否与旧值不同,如果不同,则将新值设置为响应式数据的属性值,并触发所有依赖该响应式数据的Effect重新执行。

3. 依赖收集

依赖收集是Vue3中的一项关键机制,它用于收集所有依赖于响应式数据的Effect,以便在响应式数据发生变化时触发这些Effect重新执行。

function trackEffects(effect, target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }
  let deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect);
}

trackEffects函数中,我们首先检查当前是否有Effect正在执行,如果没有,则直接返回。然后,我们获取响应式数据的依赖列表,如果不存在,则创建一个新的依赖列表并将其添加到响应式数据的依赖列表映射中。最后,我们将当前的Effect添加到依赖列表中。

4. 依赖触发

依赖触发是Vue3中用于在响应式数据发生变化时触发所有依赖该数据的Effect重新执行的机制。

function triggerEffects(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => effect());
  }
}

triggerEffects函数中,我们首先获取响应式数据的依赖列表映射,然后获取响应式数据的依赖列表,如果存在,则遍历该列表中的所有Effect并重新执行它们。

示例

为了演示Effect & Reactive的用法,我们编写了一个简单的示例:

const counter = reactive({ num: 0 });
const dummy = 0;
effect(() => (dummy = counter.num));
console.log(dummy); // 0
counter.num++;
console.log(dummy); // 1

在这个示例中,我们首先创建了一个响应式对象counter,然后创建了一个Effect函数,该函数将响应式对象counternum属性的值赋值给dummy变量,最后我们调用effect函数,将Effect函数添加到响应式对象counter的依赖列表中。

当我们调用counter.num++时,响应式对象counternum属性的值发生改变,这将触发依赖于该属性的所有Effect重新执行,其中包括我们创建的Effect函数,因此,dummy变量的值也将被更新为1。

总结

在本文中,我们实现了Vue3中经典的依赖收集和依赖触发机制,并通过测试用例验证了其正确性。通过这些机制,Vue3能够在响应式数据发生变化时自动更新视图,从而实现响应式编程。