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.