[VUE3]实现一个自己的简易Reactivity模块(一)

141 阅读5分钟

前言

VUE3各个模块依赖如下,其中VUE3响应式实现依赖于reactivity模块,我将在本文中使用TDD( Test-driven development 测试驱动开发 )的方式来实现一个自己的Reactivity模块

                                    +---------------------+
                                    |                     |
                                    |  @vue/compiler-sfc  |
                                    |                     |
                                    +-----+--------+------+
                                          |        |
                                          v        v
                      +---------------------+    +----------------------+
                      |                     |    |                      |
        +------------>|  @vue/compiler-dom  +--->|  @vue/compiler-core  |
        |             |                     |    |                      |
   +----+----+        +---------------------+    +----------------------+
   |         |
   |   vue   |
   |         |
   +----+----+        +---------------------+    +----------------------+    +-------------------+
        |             |                     |    |                      |    |                   |
        +------------>|  @vue/runtime-dom   +--->|  @vue/runtime-core   +--->|  @vue/reactivity  |
                      |                     |    |                      |    |                   |
                      +---------------------+    +----------------------+    +-------------------+

实现一个reactive

传入一个对象或者数组,reactive函数会返回一个响应式代理对象。 在实现具体代码之前,我们利用TDD的思想,先将所需的测试写出

// reactive.spec.ts
describe("reactive", () => {
  it("happy path", () => {
    const original = { foo: 1 }

    const observer = reactive(original)

    expect(original).not.toBe(observer)

    expect(observer.foo).toBe(1)
  })
})

我们现声明一个original对象,然后再用reactive函数将他包裹,original变成了一个响应式对象,我们期望original与observer不相等,并且observer对象上也有foo这个属性,值为1。

我们知道,VUE3响应式原理是根据Proxy来代理对象实现响应式,所以我们创建一个reactive函数, 返回值创建一个proxy,并且声明get与set函数

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

这时我们已经可以发现测试已经可以通过。

实现 isReactive

我们在测试中再添加一点新的API

// reactive.spec.ts
describe("reactive", () => {
  it("happy path", () => {
    const original = { foo: 1 }

    const observer = reactive(original)

    expect(original).not.toBe(observer)

    expect(observer.foo).toBe(1)
    
    expect(isReactive(observer)).toBe(true)
  
    expect(isReactive(original)).toBe(false)
  })
})

新添加isReative与isProxy这两个API,可以分析一下问题。这时候observer已经是一个Proxy对象,当访问observer时,会触发get函数。而我们的目标就是,给isReative传入一个值,来判断这个值是否是一个reactive,所以我们就可以利用这一特性来实现这个功能。

在reactive.ts中创建一个isReactive函数

export function isReactive(value) {
  return !!value[ReactiveFlags.ISREACTIVE];
}

当value访问其中的属性时,如果是一个reactive对象,就会触发proxy的get函数,因此,我们声名一个枚举类型对象RactiveFlags

// reactive.ts
export const enum ReactiveFlags {
  ISREACTIVE = "__v_reactive"
} 

在对proxy对象的get函数添加一个判断,当key为__v_reactive时,就说明使用了isReactive函数,并返回true

// reactive.ts
export function reactive(raw) {
  return new Proxy(raw, {
    get(target, key) {
    
      if ( key === ReactiveFlags.ISREACTIVE){
        return true
      }
    
      const res = Reflect.get(target, key)
      
      return res
    },
    set(target, key, newValue) {
      const res = Reflect.set(target, key, newValue)
      return res
    }
  })
}

如过isReactive传入的value上没有__v_reactive这个属性,那isReactive就会返回一个undefined,那自然就不是一个reacitive,我们再使用!!运算符将结果转化为Boolean,这是测试已经可以全部通过

实现 isProxy

我们再添加一个新的API,isProxy

describe("reactive", () => {
  it("happy path", () => {
    const original = { foo: 1 }

    const observer = reactive(original)

    expect(original).not.toBe(observer)

    expect(observer.foo).toBe(1)
    
    expect(isReactive(observer)).toBe(true)
  
    expect(isReactive(original)).toBe(false)
    expect(isProxy(observer)).toBe(true)
  })
})

我们可以利用isReactive来实现这个功能

// reactive.ts
export function isProxy(value) {
  return isReactive(value)
}

这时测试已经全部通过

实现 effect 函数

reactivity模块的实现,最重要的就是依赖收集与触发,而effect则是实现这一功能的桥梁或者说是开关。

同样,我们先写出一个测试

//  effect.spec.ts
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)
  })
})

期望effect函数可以接受一个函数,这个函数内部包括我们期望实现响应式的数据或依赖。当effect函数传入一个依赖时,我们就可以收集这个依赖,并且自动运行这个依赖。当其中的响应式数据发生改变时,我们可以监测到这个变化,并且及时更新依赖。 以上就是初步的一些关于effect函数的想法,下面我们来实现它。

我们先创建一个effect函数,并使用OOP(Object-oriented programming 面向对象编程)的思想,实例化一个ReactiveEffect对象

// effect.ts
export function effect(fn) {
  const _effect = new ReactiveEffect(fn);

  _effect.run();
}

ReativeEffect对象需要传入一个依赖函数,并且需要一个run函数来运行依赖函数。

// effect.ts
class RactiveEffect {
  private _fn;
  constructor(fn) {
    this._fn = fn;
  }

  run() {
    const result = this._fn();

    return result
  }
}

这是我们已经完成了运行依赖函数这一功能,那如何该收集这个依赖呢?对,我们可以利用前面封装的Proxy中的get与set方法来实现依赖收集与触发。

image.png 当响应式数据被访问到时,会触发get方法,这时候我们可以使用track函数将当前依赖收集储存在dep中。当该响应式对象被重新赋值时,会触发set方法,这时候我们可以使用trigger在收集起来的依赖dep中找到对应的依赖,来重新执行,这时我们就可以实现依赖的实时更新。

// 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, newValue) {
      const res = Reflect.set(target, key, newValue)
      trigger(target, key, newValue)
      return res
    }
  })
}

在get和set方法中添加track与trigger后,我们就可以实现对依赖的收集与触发。

// effect.ts

const targetMap = new Map();
export function track(target, key) {
  let depsMap = targetMap.get(target);

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

  let deps = depsMap.get(key);

  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }

  effectTrack(deps);
}

function effectTrack(deps) {
  deps.add(activeEffect);
}

export function trigger(target, key, value) {
  const depsMap = targetMap.get(target);

  const deps = depsMap.get(key);

  effectTrigger(deps);
}

function effectTrigger(deps) {
  for (let effect of deps) {
    effect.run();
  }
}

先创建一个targetMap来存放target与key的对应关系,在运行track时,先去targetMap中寻找是否有对应target的depsMap,如果没有,我们就进入初始化流程,创建一个depsMap,并将他与target对应存在targetMap中。之后,在根据key来取出对应的deps与就是依赖,如果没有依赖的化,就进入初始化流程。创建一个set来存放对应的deps。

容器有了,那依赖我们该如何获取呢?我们创建一个activeEffect来存放当前effet实例,这样我们就获取到了当前的依赖。

// effect.ts
let activeEffect;
class RactiveEffect {
  private _fn;
  constructor(fn) {
    this._fn = fn;
  }

  run() {
    activeEffect = this;
    const result = this._fn();

    return result
  }
}

之后我们就可以将activEffect存入到deps中,就获取到了当前的依赖。他们的对应关系如下

image.png 这时我们的测试已经可以全部跑通,已经初步实现了reactive与effect的依赖收集与触发功能

以上就是Reactive与effect的初步实现。在这一系列中,我将使用TDD的思想来一步步完善Reactivity模块。


这是我的github仓库mini-vue3,如果这对你有帮助就给我一个star吧。