This post was originally posted on my blog:
“A Map of Vue 3.0’s Responsive System”

With the release of Vue 3.0 Pre Alpha, we got a peek at the source code implementation. One of the most ingenious features of Vue is its responsive system, and the corresponding implementation can be found in the Repository’s Packages/Reactivity module. Although the source code is not much, and there are a lot of analysis articles on the web, it is quite a mental task to clearly understand the implementation of the reactive principle. After a day of research and sorting, I summarized the principle of the responsive system into a map, and this paper will also focus on this map to describe the specific implementation process.

I have also uploaded the code involved in the article
warehouse, combined with the code to read this article will be more fluent oh!

A basic example

Vue 3.0’s responsive system is a standalone module that can be used completely without Vue, so we can debug it directly in the Packages/Reactivity module after cloning the source code.

  1. Run in the project root directoryyarn dev reactivityAnd then enterpackages/reactivityCatalog finds outputdist/reactivity.global.jsFile.
  2. Create a new index.html and write the following code:

    <script src="./dist/reactivity.global.js"></script>
    <script>
    const { reactive, effect } = VueObserver
    
    const origin = {
      count: 0
    }
    const state = reactive(origin)
    
    const fn = () => {
      const count = state.count
      console.log(`set count to ${count}`)
    }
    effect(fn)
    </script>Copy the code
  3. Open the file in your browser and execute it on the consolestate.count++, you can see the outputset count to 1.

In the example above, we use reactive() to convert the Origin object to the Proxy object state; Use the effect() function to call fn() as a reactive callback. Fn () is triggered when state.count changes. We will use this example in conjunction with the flow chart above to illustrate how this responsive system works.

Initialization phase

During the initialization phase, two main things are done.

  1. theoriginObject is converted to a reactive Proxy objectstate.
  2. The functionfn()As a reactive effect function.

Let’s start with the first thing.

As you all know, Vue 3.0 uses proxies to replace object.defineProperty (), rewriting getters/setters for objects, and doing dependency collection and response firing. However, in this phase, we will leave aside for the moment how it overwrites the getter/setter of the object, which will be explained in more detail in the dependency collection phase. For simplicity, we can boil this down to a two-line reactive() function:

export function reactive(target) {
  const observed = new Proxy(target, handler)
  return observed
}Copy the code

Complete code in
reactive.js. Here,
handlerThat’s the key to changing getters and setters, which we’ll talk about later.

Let’s look at the second thing.

When an ordinary function fn() is wrapped with effect(), it becomes a reactive effect function, and fn() is executed immediately.

Due to thefn()There are properties that refer to the Proxy object, so this step triggers the getter for the object to start dependency collection.

In addition, the effect function can be pressed into a group called “activeReactiveEffectStack” (here effectStack) stack for subsequent depend on the collection of use.

Take a look at the code (see effect.js to complete the code) :

Export function effect(fn) {const effect = function effect(... Args) {return run(effect, fn, args)} effect() return effect} export function run(effect, fn, args) Args) {if (effectStack.indexof (effect) === -1) {try {if (effectStack.indexof (effect) === -1) {if (effectStack.push(effect) Fn () completes dependency collection by using effect return fn(... Args)} finally {// Discard this effect effectstack.pop ()}}Copy the code

At this point, the initialization phase is complete. Next comes the dependency collection phase, the most critical step of the entire system.

Dependency collection phase

This phase is triggered when effect is immediately executed and its internal FN () fires the getter for the Proxy object. Simply put, whenever a statement like state.count is executed, the getter for state is triggered.

The most important purpose of the dependency collection phase is to create a dependency collection table, known as the targetMap shown here. TargetMap is a WeakMap, whose key value is the state of the current Proxy object, and value is the depsMap corresponding to the object.

DepsMap is a Map. The key value is the property value when the getter is triggered (count), and the value is the effect for which the property value is triggered.

Or is it a little convoluted? So let’s do another example. Suppose you have a Proxy object and effect like this:

const state = reactive({
  count: 0,
  age: 18
})

const effect1 = effect(() => {
  console.log('effect1: ' + state.count)
})

const effect2 = effect(() => {
  console.log('effect2: ' + state.age)
})

const effect3 = effect(() => {
  console.log('effect3: ' + state.count, state.age)
})Copy the code

The targetMap should look like this:

The {target -> key -> deP} correspondence is established and the dependency collection is complete. The code is as follows:

export function track (target, operationType, key) { const effect = effectStack[effectStack.length - 1] if (effect) { let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } let dep = depsMap.get(key) if (dep === void 0) { depsMap.set(key, (dep = new Set())) } if (! dep.has(effect)) { dep.add(effect) } } }Copy the code

It is important to understand the dependency collection table targetMap, because this is the core of the whole responsive system core.

The response phase

Recalling the example in the previous section, we have one{ count: 0, age: 18 }And construct three effects. To see the effect on the console:

It worked as expected, so how did it work? First, take a look at the schematic of this phase:

When a property value of an object is modified, the corresponding setter is fired.

The trigger() function in the setter finds the individual DEPs for the current property from the dependency collection table and pushes them into the Effects and computedEffects queues. The effects are then executed one by one through scheduleRun().

Since the dependency collection table has been set up, it is easy to find the DEP corresponding to the attribute by looking at the code implementation:

export function trigger (target, operationType, Get (target) if (depsMap === void 0) {return} // Get (depsMap) const depsMap = targetMap effects = new Set() if (key ! Void 0) {const dep = depmap.get (key) dep && depp.foreach (effect => {effects.add(effect)})} ForEach (effect => {effect()})}Copy the code

The code here does not deal with special cases such as the length of the array being modified, for interested readers to check
Vue-next corresponding source codeOr,
This articleLook at how these situations are handled.

At this point, the reactive phase is complete.

conclusion

The process of reading the source code was challenging, but I was often amazed by some of the implementation ideas of Vue and learned a lot. According to the operation process of responsive system, this paper divides the three stages of “initialization”, “dependent collection” and “responsive”, and expounds the things done in each stage respectively, which should be able to better help readers to understand its core ideas. Finally, attach the warehouse address of the example code of the article, interested readers can play by themselves:

tiny-reactive