likes
comments
collection
share

「mini-vue3」实现 reactive & effect 的「收集依赖/触发依赖」功能

作者站长头像
站长
· 阅读数 75

背景

本文记录笔者实现 mini-vue3 项目的 reactiveeffect 模块 收集依赖触发依赖功能的过程,中间涉及ProxyReflectSet, Map等知识。

思路

最基本的思路还是以测试来驱动开发,但是要分 2 个部分考虑,一个是收集依赖部分,一个是触发依赖部分。

由于项目高度模拟 vue3 源码,所以在开始之前,需要先有一个 reactiviy 模块来创建响应式对象

初始化 reactivity 模块

函数签名

根据 vue3 源码,reactive 函数接收一个原始对象,返回一个响应式对象

// src/reactivity/reactive.ts
export function reactive(target: any) {
  return new Proxy(target, {})
}

测试驱动开发

编写测试用例

先把 reactive 最基本的功能列一下,也就是 happy path:

  1. 原始对象响应式对象 是不相等的。 (target !== proxy)
  2. 调用 响应式对象 的属性时,能得到 原对象 的同名属性值。 (proxy.key === target.key

现在可以编写测试文件了。

// src/reactivity/tests/reactive.spec.ts

import { reactive } from '../reactive'

/*
  describe 定义 reactive 组,可以包含多条测试
*/
describe('reactive', () => {

  /*
     it 定义单个测试
     happy path 指的是 `模块要处理的最基本逻辑点`,比如下面要测试的 reactive 的 happy path:

      1. 原对象 和 响应式对象 是不相等的。 (target !== proxy)
      2. 调用 响应式对象 的属性时,能得到 原对象 的同名属性值。 (proxy.age === target.age)
  */
  it('happy path', () => {
    const target = { age: 10 };
    const proxy = reactive(target);
    expect(proxy).not.toBe(target);
    expect(proxy.age).toBe(target.age);
  })
})

实现 reactive 函数

现在,以测试来驱动开发,实现 reactive 函数。

// src/reactivity/reactive.ts
export function reactive(target: any) {
  return new Proxy(target, {
  
    get(target, key) {
      return Reflect.get(target, key);
    },
    
    set(target, key, value) {
      return Reflect.set(target, key, value)
    }
  })
}

这里通过 Proxy + Reflect 来实现,Reflect 可以和 Proxy 很好地结合使用,参考 阮一峰老师的博文

Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。

比如,如果 get 函数接受了第 3 个参数 receiver,可以直接调Reflect.get(target,key,receiver),这是 target[key] 做不到的。

执行测试,验证效果

执行 jest 测试 reactive 模块,效果如下:

「mini-vue3」实现 reactive & effect 的「收集依赖/触发依赖」功能

测试通过✅,基础 reactivity 模块已实现。

实现「收集依赖」

本功能需要:

  1. 借助 effect 来收集依赖函数
  2. 依赖函数中触发响应式对象的 GET 操作。
  3. GET 操作中,把 依赖函数 封装成结点,加入该属性的容器中。

实现 effect

编写测试用例

待测试的功能有:

  1. effect 函数接收一个依赖函数fn
  2. 首次调用 effect 时执行 1fn

我们结合刚刚实现的 reactive 模块来编写测试代码:

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

describe('effect', () => {

  it('happy path', () => {
    const user = reactive({ age: 10 });

    let nextAge = 0;
    effect(() => {
      nextAge = user.age + 1;
    })
  
    expect(nextAge).toBe(11);
  })
  
})

fn 执行时,nextAge 得到的值应该是 user.age + 1,也就是 11

通过 effect 执行依赖函数

主要就是暴露一个 effect 函数,接收 fn,执行 1fn

根据 vue3源码,我们要把 fn 封装成一个结点,所以还需要定义一个 ReactiveEffect 类,通过它暴露的方法来执行 fn

// src/reactivity/effect.ts

class ReactiveEffect {
  private _fn: Function;
  
  constructor(fn: Function) {
    this._fn = fn;
  }

  public run(): void {
    if (!this._fn) return;
    this._fn();
  }

}

export function effect(fn: Function) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
}

执行测试,验证效果

编写完成,测试 effect 结果如下,已通过✅

「mini-vue3」实现 reactive & effect 的「收集依赖/触发依赖」功能

收集依赖结点到属性容器

effect 收集到当前依赖结点后,响应式对象GET 操作应该把这个结点收集到当前属性容器中。

保存当前依赖结点

通过一个全局变量activeEffect保存当前依赖结点

// src/reactivity/effect.ts

let activeEffect: ReactiveEffect;

class ReactiveEffect {
  private _fn: Function;
  
  constructor(fn: Function) {
    this._fn = fn;
  }

  public run(): void {
    if (!this._fn) return;
    activeEffect = this;
    this._fn();
  }
}

收集结点到容器

收集结点容器需要一些数据结构的设计,具体如下:

  1. targetMap(对象表) 保存所有对象它们的属性表,形如 Map<target,keyMap>
  2. keyMap(属性表) 保存所有属性它们的依赖集,形如 Map<key, ReactiveEffectSet>
  3. ReactiveEffectSet(依赖集) 保存这个属性的所有依赖结点,形如Set<ReactiveEffect>,使用 Set 有一个好处,就是一个结点被添加了以后,再添加它不会加入了,这样依赖结点就不会重复。

接下来实现它:

// src/reactivity/effect.ts

let activeEffect: ReactiveEffect;

class ReactiveEffect {
  private _fn: Function;
  
  constructor(fn: Function) {
    this._fn = fn;
  }

  public run(): void {
    if (!this._fn) return;
    activeEffect = this;
    this._fn();
  }
}

export function effect(fn: Function) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();
}

// 完整的容器
const targetMap = new Map<Object, Map<string, Set<ReactiveEffect>>>();

// 收集依赖结点
export function track(target: Object, key: string) {
  let keyMap = targetMap.get(target);
  if (!keyMap) {
    keyMap = new Map();
    targetMap.set(target, keyMap);
  }

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

  effectSet.add(activeEffect);
}

通过 GET 接入收集流程

依赖函数执行时,会触发响应式对象GET逻辑,此时接入收集结点到容器的流程,这样整个过程就连起来了。

import { track } from "./effect";

export function reactive(target: any) {
  return new Proxy(target, {

    get(target: Object, key: string) {
      const res = Reflect.get(target, key);
      // 增加 track 函数,接入收集流程
      track(target, key);
      return res;
    },

    // ...
  })
}

实现「触发依赖」

编写测试用例

测试触发依赖,需要先收集依赖,再测试响应式对象属性值变化时,依赖函数是否执行。

测试用例编写如下:

// src/reactivity/tests/effect.spec.ts

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

describe('effect', () => {

  it('track and trigger', () => {
    const user = reactive({ age: 10 });

    let nextAge = 0;
    effect(() => {
      // 收集依赖
      nextAge = user.age + 1;
    })
    // 测试 fn 是否首次执行
    expect(nextAge).toBe(11);
    
    // 触发依赖
    user.age++;
    // 测试触发依赖
    expect(nextAge).toBe(12);
  })
  
})

测试驱动开发

有了收集依赖的基础,触发依赖非常简单。

响应式对象的属性值被设置时,触发 SET 操作,此时遍历该属性容器的所有依赖结点,都执行一遍依赖函数即可。

遍历结点触发依赖

通过 targetMep -> keyMap -> ReactiveEffectSet 获取到依赖集,遍历其中的依赖结点,触发所有的依赖,这样就实现了触发依赖功能,这个流程在 effect 模块中处理:

// src/reactivity/effect.ts
// ...

export function trigger(target: Object, key: string) {
  let keyMap = targetMap.get(target);
  if (!keyMap) return;

  let effectSet = keyMap.get(key);
  if (!effectSet) return;

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

通过 SET 接入触发流程

响应式对象的属性被设置时,执行 SET 操作,接入 effect 模块提供的 触发该属性的所有依赖的流程:

// src/reactivity/reactive.ts
import { track, trigger } from "./effect";

export function reactive(target: any) {
  return new Proxy(target, {
    // ...
    set(target: Object, key: string, value: unknown) {
      const res = Reflect.set(target, key, value);
      trigger(target, key);
      return res;
    }
  })
}

验证效果

通过 jest 运行 effect 测试用例,结果如下:

「mini-vue3」实现 reactive & effect 的「收集依赖/触发依赖」功能

测试通过✅,触发依赖功能已完成 🎉

原理示意图

笔者针对 mini-vu3 的实现,绘制了一张原理图帮助思考🍺

「mini-vue3」实现 reactive & effect 的「收集依赖/触发依赖」功能

总结

本文高度模仿 vue3源码reactiveeffect 模块,实现了 收集依赖/触发依赖 功能,项目完整代码可在笔者的远程库 ---- mini-vue3 完成收集依赖与触发依赖 中查看。

转载自:https://juejin.cn/post/7196517835380506685
评论
请登录