preface

After the previous exploration, we basically understand the general content of Vue3 responsiveness, so to consolidate, to implement a simple reactivity responsiveness.

Achieve the goal

Suppose the following data exists


  let rawData = 0
  let reactData=rawData+5

Copy the code

When modifying rawData, we want to update reactData synchronously, so we can change the above code to:

  function reactive(){
    reactData = rawData+5
  }
Copy the code

In this way, when we modify rawData, we can call Reactive () and get the modified reactData. This is actually the simplest response.

@vue/reactivity

Vue’s Reactivity module can stand alone, so let’s review its usage and write a simple reactivity using it as a template

We can directly install the Reactivity module: NPM i@vue/reActivity

  const {effect,reactive} =require('@vue/reactivity')
  // Declare a reactive object
  let  rawData = reactive({
    value:1
  })
  let reactData;
  // Rely on collection
  effect(() = >{
    reactData = rawData.value+1
    console.log('reactData:',reactData)
  })
  rawData.value = 4/ / output reactData: 5

Copy the code

As we have seen above, rawData processed by reactive reactive automatically updates reactData when it is modified. That’s what we want in the end. So let’s get this straight.

Implement your own Reactivity

  • First, we need a way to collect and trigger dependencies.

    • Collection dependencies are the logic associated with collecting responsive data.
    • Triggering dependencies is the execution of reactive logic when our reactive data changes.
  • Second, we also need a place to store collected dependencies. An additional point is the need to prevent duplicate collection of dependencies.

Simple version of

Now let’s start with a simple version:

// Define a global variable whose purpose is to associate the collection dependency method with the Depend in deP
let currentEffect;
class Dep = {constructor(val){
    // Store the default value we passed, i.e
    this._val =val
    // Used to store collected dependencies
    this.effects = new Set(a)// We use Set to avoid collecting duplicate dependencies.
  }
  / / value
  get value() {return this._val
  }
  / / assignment
  set value(newVal) {this._val = newVal
  }
  // Rely on collection
  depend(){
    if(currentEffect){
      this.effects.add(currentEffect)
    }
  }

  // Trigger dependencies
  notice(){
    this.effects.forEach(effect= >effect())
  }
}

Copy the code

The above code simply implements dependency triggering and collection and is the simplest framework. Let’s take a look at the usage:


const rawData =new Dep(5)
function effect(eff){
  currentEffect =eff
  // Trigger dependencies directly
  eff()
  // Triggers dependency collection
  rawData.depend()
  / / empty currentEffect
  currentEffect =null
}
let reactData;
effect(() = >{
  reactData = rawData.value+5
  console.log('reactData:',reactData)
})

rawData.value = 10
rawData.notice()/ / output reactData: 15
Copy the code

As a small detail, we use the global variable currentEffect to interact with the Depend dependency collection in Reactive rather than directly calling the Depend method of Reactive. I’ll explain why later.

After the above code runs, we can basically achieve our function. This implementation requires us to manually invoke the notice to trigger the dependency. The dependency collection that requires us to display is obviously not in line with our requirements. So we’re working on the next step.

Advanced: Automatic collection of dependencies and automatic triggering of dependencies

The purpose of this step is to automate our triggering dependencies and dependency collection without having to invoke them manually.

This step is actually very simple, but also involves some basic concepts in Js, if there is not too clear can refer to the MDN about get and set related concepts. This is data hijacking via the get and set features, so that we can encapsulate dependent triggers and collections into corresponding functions:

  class Dep{...get value() {this.depend()// Triggers the collection of dependencies
      return this._val
    }

    set value(newVal) {this._val = newVal
      this.notice()
    }
    ...
  }
Copy the code

After the above improvements, the following use actually eliminates the need to manually trigger dependencies and dependency collection:

const rawData =new Dep(5)
function effect(eff){
  currentEffect =eff
  eff()// The dependency collection process is automatically triggered in the eff because the get method for reactive data is called.
  currentEffect =null
}
let reactData;
effect(() = >{
  reactData = rawData.value+5
  console.log('reactData:',reactData)
})

// Because the reactive data changes, the set method is triggered, which completes the process of executing the dependency.
rawData.value = 10/ / output reactData: 15
Copy the code

Further up: Implementing reactive in the official version

Above we implemented automatic collection and execution of dependencies, but the problem is that reactive data can only be of a single type and can only be attached to the value property, which is more like ref in Vue3. So what we’re going to do is we’re going to need to support object responsiveness.

Here are two additional ways to implement hijacked objects:

  • Object.defineproperties () : This is used to define new properties on an Object or modify existing properties. Vue2 implements data hijacking in this way.

    • Limitations: You can’t hijack new attributes, you can’t hijack array length changes. In addition, because it hijacks attributes, we need to recurse when we have layers of nested objects, which is also a very performance consuming thing.
  • Proxy: Proxy is a direct Proxy object. There is no difference in functionality between this and Object.defineProperties, it is hijacking. It’s just that the proxy intercepts object changes directly, so it avoids the pits of defineProperties.

    • An extra note here is Reflect, which usually comes in a pair with Proxy. It provides methods to intercept JS operations

With that foundation in place, let’s rework our reactive:

//targetMap is used to store dependencies and data mappings
const targetMap = new Map(a)// Get dependencies
function getDep(target,key){
  let deps = targetMap.get(target)
  // Initialize it the first time
  if(! deps) { deps =new Map()
    targetMap.set(target, deps)
  }
  let dep = deps.get(key)
  // The first time the dep does not exist, initialize it
  if(! dep) { dep =new Dep()
    deps.set(key, dep)
  }
  // All this stuff is for key to map to deP
  return dep
}

  // Wrap responsive data
function reactive(raw) {
    return new Proxy(raw, {
      // Get collects dependencies
      get(target, key) {
        const dep = getDep(target, key)
        dep.depend()
        return Reflect.get(target, key)
      },

      set(target, key, value) {
        // The main function of set is to trigger dependencies
        // Get the deP object
        const dep = getDep(target, key)
        / /! Here the value needs to be updated before the update is notified
        const result = Reflect.set(target, key, value)
        dep.notice()
        return result
      }
    })
}

EffectWatch needs to be streamlined here
function effectWatch(effect) {
  currentEffect = effect
  // The collected dependency functions are called the first time the collection is performed
  effect()
  / / empty
  currentEffect = null
}
Copy the code

Here, we’ve introduced Reactive to encapsulate reactive data so that our responses can support complex object types. Here we introduce a Map to store the mapping of data and Dep dependencies. One important thing to note here is that targetMap stores a Map, which is used to store the mapping of object properties and dependencies.

To summarize the principle of our Reactive: we use Proxy to hijack the reactive data that needs to be processed to form a mapping of data and dependencies. We then collect the dependencies as we fetch the data and perform the dependencies as we update the data. Let’s use it:

  let demo = reactive({
    num:1
  })
  let reactiveData
  effectWatch(() = >{
    reactiveData = demo+2
    console.log(reactiveData)
  })

  demo.num = 4
  / / output
  / / 3
  / / 6
Copy the code

conclusion

So that’s our response formula for min-vue. Reactive is an idea, and the reactive we use in Vue basically involves updating views, which is just one aspect of it. This is why Vue3 has a separate reactive module. Now that we have this kind of responsive thinking, we can apply it in many more places. Later, we will slowly realize our own min-Vue with the interface rendering.

PS: Attach the code address at the end