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,runnerIs its only argument.
  • performstop(runner)Later, when the reactive object changes, it is not executed againeffect(fn)In thefnFunction.
  • Performed againrunner().dummyIt 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.stopfunction

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,runnerThe bindingeffectThe 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, toReactiveEffectClass to add astopmethods

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.