Stop
This function should not say anything more, look at the literal meaning can also guess a nine or nine out of ten, but as a programmer, we still want to rigorous thinking, old rules, we still look at a single test.
A single measurement
Next, you will follow the single test of the code, line by line, there will be in my own understanding of the comments, I hope you read the single test to help.
// src/reactivity/effect.spec.ts
describe("effect".() = >{... it("stop".() = > {
let dummy;
// Create the responsive object obj
const obj = reactive({prop: 1})
// This is the previous runner, when executed again is equivalent to executing fn again
const runner = effect(() = > {
dummy = obj.prop
})
// Change the prop value of the responsive object obj
obj.prop = 2
// The dummy will change as well
expect(dummy).toBe(2)
// This is a function with a single parameter, which is our runner
stop(runner)
// We update the value of the reactive object again
obj.prop = 3
Dummy has not changed this time. This is the main function point we will implement today
expect(dummy).toBe(2)
// execute runner again and dummy updates again
runner()
expect(dummy).toBe(3)})})Copy the code
By reading the single test above, we can draw a conclusion
- Stop is a function,
runner
Is its only argument. - perform
stop(runner)
Later, when the reactive object changes, it is not executed againeffect(fn)
In thefn
Function. - Performed again
runner()
.dummy
It will be updated again.
Analysis of the
After the set operation is triggered, the dependency will be triggered automatically. How can we make it stop triggering? If we remove the current effect from the targetMap, we will not trigger the dependency if we cannot find the effect associated with it.
coding
Armed with the above conclusions and our own analysis, let’s implement the code logic for Stop step by step.
1.stop
function
Since stop is a function, we first export a function in effect.ts.
// src/reactivity/effect.ts
export function stop (runner) {
runner.effect.stop()
}
Copy the code
In the above code, we called runner. Effect, but the runner we returned did not have effect.
2,runner
The bindingeffect
The instance
Now, let’s modify effect to add effect to the returned runner.
export function effect (fn) {
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run()
// Mount the current 'effect' instance to runner
const runner:any = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
Copy the code
Thus, the stop() function actually executes the stop method in the ReactiveEffect class.
3, toReactiveEffect
Class to add astop
methods
Through the above series of operations, we have associated the corresponding effect according to runner.
? “> < p style =” max-width: 100%; clear: both; min-height: 1em; When we rely on the collection, we collect the effect to the corresponding DEPS, and then we can store the DEPS to the effect.
// src/reactivity/effect.ts.export function track (target, key) {... activeEffect.deps.push(dep)// Store the DEP in the currently created effect}...class ReactiveEffect {... deps = []// Define a dePS to store the collection of current 'effect' DEPs. public stop () {// Here we need to clear the current effect
cleanUpEffect(this)}}function cleanUpEffect (effect) {
effect.deps.forEach((dep:any) = > {
dep.delete(effect)
})
}
...
Copy the code
Now, we have completed the basic function of Stop, but consider a problem, when we call stop many times, each time to clear our DEPS, this must be more or less affected performance, let’s make a simple optimization of Stop, add an active state, let it only clear once.
// src/reactivity/effect.ts
class ReactiveEffect{... active =true // Define a state to determine whether it has been cleared. Default is true, indicating that it has not been cleared. } public stop () {// If it is not already empty, we perform the empty operation
if (this.active) {
cleanUpEffect(this)
// Change the state to empty. The next time stop is executed, the operation will not be performed again
this.active = false}}Copy the code
At this point, the stop function is complete.
The test results
TypeError: Cannot read property 'deps' of undefined
46 | }
47 | dep.add(activeEffect)
> 48 | activeEffect.deps.push(dep)
| ^
49 | }
50 |
51 | export function trigger (target, key) {
at track (src/reactivity/effect.ts:48:16)
at Object.foo (src/reactivity/reactive.ts:6:7)
at Object.<anonymous> (src/reactivity/tests/reactive.spec.ts:9:16)
Copy the code
ActiveEffect may be undefined, so we find the corresponding code.
? ActiveEffect is undefined because it is not called reactive. Spec. ts.
To solve the problem
How to solve the above problem? If it is undefined, we don’t want it to trigger dependency collection. After all, there is no need to collect dependencies if there is no effect involved.
// src/reactivity/effect.ts
export function effect (target, key) {...if(! activeEffect)return // Check here
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
Copy the code
Run the tests again, and all the tests pass, indicating that our problem is solved.
Optimize the code
Let’s change the stop test slightly
// src/reactivity/effect.spec.ts
describe("effect".() = >{... it("stop".() = >{...// obj.prop = 3
obj.prop ++ // Make it so. })})Copy the code
When I run the test again, stop stops working again.
Expected: 2 Received: 3 77 | obj. Prop++ 78 | / / found dummy did not change this time, that is what we are today in order to achieve the main function of > 79 | expect (dummy). Place (2) | 80 | ^ / / again runner, not surprisingly, Dummy update again 81 | runner (82) | expect (dummy). Place (3)Copy the code
Let’s think about why this is happening. Just change obj.prop=3 to obj.prop++, which should be the same thing, but it still triggers the dependency.
Prop ++ = obj.prop+ 1. Obj.prop + 1 = obj.prop+ 1 = obj.prop+ 1 = obj.prop+ 1 = obj.prop+ 1
Optimize the code
First, we use a global variable shouldTrack to control whether we currently need to collect dependencies.
// src/reactivity/effect.ts
let shouldTrack:boolean = false.export function track (target, key) {...if(! activeEffect)return
if(! shouldTrack)return // Add this sentence
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
...
Copy the code
After dealing with the dependency collection logic, we also need to assign shouldTrack when appropriate.
We all know that effect(FN) is called when a dependency is triggered by an assignment to a reactive object. This FN is run in ReactiveEffect. In fn, the get operation of the reactive object is triggered to collect the dependency again. So the best thing to do is actually to restrict when we run.
// src/reactivity/effect.ts
public run () {
if (!this.active) {
// Stop
return this._fn()
}
// Normal status
shouldTrack = true
activeEffect = this
const result = this._fn()
shouldTrack = false
return result
}
Copy the code
ShouldTrack (true), then this._fn(), and then close it. When stop, there is no action to open shouldTrack, so when this._fn(), ShouldTrack is still off (false) when collecting dependencies internally, so dependencies are not collected.
conclusion
In this section, we made some modifications to track and Effect before. When effect returned to runner, we incidentally bound effect to it, and when collecting dependencies, we stored effect on the corresponding DEPS container and mounted DEPS on Effect. When we execute stop(runner), we can get its own effect through runner first, and then get the dePS mounted on effect. Finally, we can delete effect in this DEPS.
Later, we added a judgment condition for dependency collection, using the variable shouldTrack to control the timing of dependency collection. After stop, we want to avoid effect(FN) from collecting dependencies when the get request is executed internally.
In the next section, let’s add onStop.