mini-vue之reactivity库的实现(一)

1,279 阅读4分钟

reactivity库的实现----reactive

测试文件

测试文件可以更好的帮我们检测实现的功能是否正确,而且更加有助于我们重构和优化代码,所以测试文件是非常必要的。

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模块(可能这时还没有实现,那就先放着)。测试文件中,定义了一个original,并且通过调用reactive返回一个响应式对象observed,此时我们希望得到的结果是observed不等于original,希望observed.foo等于1。这些都是我们希望得到的结果 ,下面来实现一下这个简单的功能。

reactive的实现

reactive get
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
      return res
    }
  })
}

我们都知道vue3实现响应式是用proxy,测试文件中值涉及到了数据的读,所以实现get功能就可以了,此时这个测试文件就可以通过了。

effect

此时我们想要去实现set的功能,那么就需要考虑到依赖的收集和触发,可以集合effect来实现。下面我们看下也给测试用例。

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


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

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

    expect(nextAge).toBe(11)
  })
})

这个测试用例主要想实现的功能很简单,effect这个函数,传一个函数,而这个函数被立即调用,所以定义的变量nextAge等于11。而这仅仅是我们希望得到的结果。下面看下怎么实现。

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


  run() {
    this._fn()
  }
}


export function effect(fn) {
  const _effect = new ReactiveEffect(fn)

  _effect.run()

}

首先定义effect函数,基于面向对象的思想,我们抽离出一个类ReactiveEffect,这个类接收fn,并定义一个run方法去执行fn。这种实现方式一目了然,我们的测试用例就可以通过了。

此时我们只是实现了effect第一层,下面我们想实现的是,当fn中的响应式数据发生变化的时候,怎么去通知fn再次执行呢?下面我们添加一下测试用例。

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


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

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

    expect(nextAge).toBe(11)
    
    user.age++
    expect(nextAge).toBe(12)
  })
})

此时我们的测试用例加了一步,当user.age++期望nextAge的值为12。如何让这个成立呢?不就是让这个fn再次执行一下吗?所以我们这里就要考虑到响应式数据的依赖和收集。

reactive 依赖收集

user.age是一个响应式数据,当执行effect执行的时候,effect的参数fn会立即执行,这是会访问到我们的响应式数据age,那么就会触发get,这时候就应该去把age这个响应式对应的依赖收集起来。

import { track } from "./effect"
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
      const res = Reflect.get(target, key)
      track(target, key)
      return res
    }
  })
}

对于依赖的收集和触发统一放在effect文件中操作


let activeEffect
class ReactiveEffect {
  ···
  run() {
    activeEffect = this
    this._fn()
  }
}

const targetMap = new Map()
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)
  }
  dep.add(activeEffect)
}

回到effect文件中,我们添加一个函数track,和一个变量activeEffectactiveEffect是一个全局的变量,在执行run方法时被赋值this,这个就是我们要收集的依赖。在track中,我们把依赖收集到Map中。大致的对应关系应该是这样的。

image.png

reactive set

当响应式数据发生变化的时候,那么就就会触发依赖更新。当user.age++的时候,会触发set,并且执行trigger方法去触发依赖的更新。

import { track,trigger } from "./effect"
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, val) {
      const res = Reflect.set(target, key, val)
      trigger(target, key)
      return res
    }
  })
}
reactive 触发依赖更新

下面看下trigger的实现


let activeEffect
class ReactiveEffect {
  ···
}

const targetMap = new Map()
export function track(target, key) {
  ···
}

export function trigger(target, key) {
  let depsMap = targetMap.get(target)

  let dep = depsMap.get(key)

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

}

export function effect(fn) {
  ···
}

当执行trigger的时候,会通过参数targetkeyMap中找到对应的依赖dep,dep是一个数组,因为我们一个响应式数据对应的可能不止是一个依赖,所以我们要把相关的所有的依赖收集到dep,当我们需要触发依赖的时候,只需要去遍历dep,拿到每一个依赖,也就是之前收集的activeEffect,并执行run就OK了。此时测试用例就通过了。

我们简易版的reactive就完成了。大家可以动手试一试哦。后续还会有更多的文章输出。

欢迎大家加入崔学社

如果大家想去了解更多的相关知识,我推荐一位大佬,大家可以加他wx:cuixr1314,进入崔学社一起学习哈。这位大佬自己一人实现了一个比较全面的mini-vue,对vue3的了解还是比较全面的,而且有mini-vue的课程哦。