Reactivity library implementation —- Reactive

The test file

The test file can better help us detect whether the function of the implementation is correct, and more help us to reconstruct and optimize the code, so the test file is very necessary.

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);
  });
});
Copy the code

The first thing we should do is implement the test file and introduce our own implementation of the Reactive module (which may not be implemented at this point, so leave it). In the test file, an original is defined, and a responsive object observed is returned by calling reactive. At this time, we hope to get the result that Observed is not equal to Original, but that observed. Foo is equal to 1. These are the results we want to achieve, so let’s implement this simple function.

The realization of the reactive

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

We all know that VUe3 uses proxy to achieve responsiveness. The value in the test file involves data reading, so it is ok to achieve get function, and the test file can pass at this time.

effect

At this point, we want to realize the function of set, so we need to consider the collection and triggering of dependencies, which can be achieved by collecting effect. Let’s look at the test cases as well.

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)})})Copy the code

The main function of this test case is very simple. Effect is a function that is called immediately, so nextAge is equal to 11. And that’s just what we’re hoping for. So let’s see how we do that.

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


  run() {
    this._fn()
  }
}


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

  _effect.run()

}
Copy the code

First, we define the effect function. Based on object-oriented thinking, we isolate a class ReactiveEffect that receives FN and define a run method to execute FN. This implementation is straightforward, and our test case should pass.

At this point, we only realized the first layer of effect. What we want to achieve is, how to inform FN to execute again when the responsive data in FN changes? Let’s add a test case.

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)})})Copy the code

At this point our test case adds a step when user.age++ expects nextAge to be 12. So how do we make this work? I’m just going to run this fn again, right? So we need to consider the dependency and collection of responsive data here.

Reactive dependency Collection

Age is a reactive data. When effect is executed, the fn parameter of effect will be executed immediately, which will access our reactive data age. Then get will be triggered.

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
    }
  })
}
Copy the code

The collection and triggering of dependencies are handled in an Effect file


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

const targetMap = new Map(a)export function track(target, key) {
  // Mapping
  // 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)
}

Copy the code

Back in the effect file, we add a function track and a variable activeEffect. ActiveEffect is a global variable that is assigned this when the run method is executed. This is the dependency we will collect. In Track, we collect dependencies into a Map. The rough correspondence should look something like this.

reactive set

When reactive data changes, dependency updates are triggered. When user.age++, set is triggered and the trigger method is executed to trigger dependent updates.

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
    }
  })
}
Copy the code
Reactive triggers dependency updates

Let’s look at the implementation of trigger


let activeEffect
class ReactiveEffect {...}const targetMap = new Map(a)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) {...}Copy the code

When trigger is executed, the corresponding dependency DEP will be found in Map through the parameters target and key. Dep is an array. Since there may be more than one dependency corresponding to one responsive data, we need to collect all related dependencies into DEP. Just go through the DEP, grab each dependency, the ActiveEffects you collected earlier, and run. At this point the test case passes.

Reactive is what we do in a very simple version. You can give it a try. More articles will follow.

Welcome to join Cui Xueshe

If you want to know more about the relevant knowledge, I recommend a big guy, you can add him WX: CuIXR1314, into cui Xueshe study together. This big guy realized a relatively comprehensive mini-Vue by himself. He has a comprehensive understanding of VUE3, and there are courses of Mini-Vue.