vue3 source code analysis (1) – reactive responsive implementation

Foreword

This article is the first article in the vue3 source code analysis series. The overall implementation of the project code refers to the v3.2.10 version. The overall project architecture can refer to the article I wrote before about rollup to implement multi-module packaging. Without further ado, let’s start this series of articles with a simple example.

Give an example

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>reactive</title>
</head>
<body>
<div id="app"></div>
<!--Code of responsive module-->
<script src="../packages/reactivity/dist/reactivity.global.js"></script>
<script>
  let {<!-- --> reactive, effect } = VueReactivity;
  const user = {<!-- -->
    name: 'Alice',
    age: 25,
    address: {<!-- -->
      city: 'New York',
      state: 'NY'
    }
  };
  let state = reactive(user)

  effect(() => {<!-- -->
    app.innerHTML = state.address.city
  });
  setTimeout(() => {<!-- -->
    state.address.city = 'California'
  }, 1000);
</script>
</body>
</html>

Through the example, you can see that the data view changes after 1 second. How to achieve this effect in vue3? Let’s start with the reactive function in the example.

reactive

The reactive function returns a reactive proxy for an object. And reactive transformations are “deep”, affecting all nested properties.

import {<!-- --> reactiveHandlers } from './baseHandlers'

const reactiveMap = new WeakMap()

function reactive (target) {<!-- -->
  return createReactiveObject(target, reactiveHandlers, reactiveMap)
}

function createReactiveObject (target, baseHandlers, proxyMap) {<!-- -->
  if (!isObject(target)) {<!-- -->
    return target
  }
  // target already has corresponding Proxy
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {<!-- -->
    return existingProxy
  }
  const proxy = new Proxy(target, baseHandlers)
  proxyMap.set(target, proxy)
  return proxy
}

There are a few details to note here:

  1. If an object has been created as a reactive object, the reactive object is returned directly to avoid repeatedly proxying the same target object.

  2. Using WeakMap can avoid memory leak problems, because when the target object target is no longer referenced, its corresponding proxy object existingProxy will also be automatically Garbage collection.

reactiveHandlers

reactiveHandlers is the key object to implement responsive data. It monitors and updates data by intercepting the operations of objects.

const reactiveHandlers = {<!-- -->
  get,
  set
}

get

When we access a property of a responsive object, the get method will be triggered, which will collect dependencies on the property and associate the responsive object with the property.

import {<!-- --> isObject } from '@vue/shared'

const get = createGetter()

// shallow whether to perform shallow responsive processing
// isReadonly Is it read-only?
function createGetter (isReadonly = false, shallow = false) {<!-- -->
  return function get (target, key, receiver) {<!-- -->
    const res = Reflect.get(target, key, receiver)
    track(target, 'get', key) // Collect dependencies
    // lazy proxy
    if (isObject(res)) {<!-- -->
      return reactive(res) // recursion
    }
    return res
  }
}

There will be a lazy proxy processing operation. The specific benefits include:

  1. Performance optimization: The proxy will only be triggered when needed, avoiding unnecessary proxy and responsive data updates, and improving the performance of component rendering.

  2. Reduce unnecessary memory overhead: Responsive data objects will only be created when needed, avoiding unnecessary memory overhead.

  3. More flexible: Lazy proxies can control which data needs to be proxied and which data does not need to be proxied in a more fine-grained manner, and can handle component status more flexibly.

set

When we modify a property of a responsive object, the set method will be triggered, which will update the value of the property and notify all components that depend on the property to update.

import {<!-- --> hasChanged } from '@vue/shared'

const set = createSetter()

// shallow whether to perform shallow responsive processing
function createSetter (shallow = false) {<!-- -->
  return function set (target, key, value, receiver) {<!-- -->
    // Pay attention to reading first and then setting
    const oldValue = Reflect.get(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // Revise
    if (hasChanged(value, oldValue)) {<!-- -->
      // trigger update
      trigger(target, 'set', key, value, oldValue)
    }
  return result
  }
}

effect

The effect function is mainly used to create side-effect functions of responsive data. This function accepts a function as a parameter. The function runs once immediately. When any of the reactive properties are updated, the function will run again. The advantage of using the effect function is that it can simplify the code relationship between responsive data and side effects, making the code easier to understand and maintain.

let uid = 0
let activeEffect //Save the current effect
let effectStack = [] // Define a stack structure to solve the problem of effect nesting

type ReactiveEffectOptions = {<!-- -->
  lazy?: boolean
  scheduler?: (...args: any[]) => any
}

// lazy whether to delay the execution of side effect functions
// scheduler specifies the asynchronous task scheduler, which can be used for throttling, anti-shaking, etc.
function effect (fn, options?: ReactiveEffectOptions) {<!-- -->
  const _effect = createReactiveEffect(fn, options)
  if (!options || !options.lazy) {<!-- -->
    _effect() // executed by default
  }
  return_effect
}


//Create a function with responsive capabilities
function createReactiveEffect (fn, options) {<!-- -->
  const effect = function reactiveEffect () {<!-- -->
    // Ensure the uniqueness of the effect
    if (!effectStack.includes(effect)) {<!-- -->
      try {<!-- -->
        effectStack.push(effect)
        activeEffect = effect
        return fn() // Execute user-defined method and return value
      } finally {<!-- -->
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  }
  effect.id = uid + + // Difference effect
  effect.fn = fn // Save user method
  effect.options = options // Save user attributes
  return effect
}

In the effect function, the effectStack array is used to ensure the uniqueness of the effect function. Before each execution of the effect function, the effect function will be pushed into the effectStack array to determine whether the same already exists. >effect function. If not present, sets the current effect function to activeEffect, executes the user-defined method fn() and returns the result. During this process, use the try...finally statement block to ensure that the effectStack array can be maintained correctly.

track

The track function is to track the access of properties in the reactive object and save the relationship between the tracked dependencies (target) and side effects (activeEffect) Relationship.

let targetMap = new WeakMap()

function track (target, type, key) {<!-- -->
  if (activeEffect === undefined) return
  // get effect
  let depsMap = targetMap.get(target)
  if (!depsMap) {<!-- -->
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {<!-- -->
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {<!-- -->
    dep.add(activeEffect)
  }
}

trigger

The trigger function is used to trigger an update on a responsive object to re-render the related view.

function trigger (target, type, key, newValue?, oldValue?) {<!-- -->
  const depsMap = targetMap.get(target)
  if (!depsMap) {<!-- -->
    // never been tracked
    return
  }
  let deps = []
  // schedule runs for SET | ADD | DELETE
  if (key !== undefined) {<!-- -->
    deps.push(depsMap.get(key)) // [Set[activeEffect]]
  }
  // side effect function
  const effects = []
  for (const dep of deps) {<!-- -->
    if (dep) {<!-- -->
      effects.push(...dep)
    }
  }
  for (const effect of effects) {<!-- -->
    effect()
  }
}

The function gets the dependency graph depsMap associated with target from targetMap. If depsMap does not exist, that is, the target object has never been tracked, then it will be returned directly. Then according to the operations type and key, the related dependency collection (deps) is obtained from depsMap. It should be noted that the deps collection stores effect instead of specific responsive objects. This is because a side-effect function can depend on multiple reactive objects at the same time. Therefore, when triggering an update, you need to first obtain the deps collection related to the target object from depsMap, and then Then get all the side effect functions from the dep collection, and finally execute them uniformly.

Illustration of the execution process

In order to facilitate understanding, the data structures corresponding to different stages are recorded below in the form of pictures.

Initial execution


Data update

setTimeout(() => {<!-- -->
  state.address.city = 'California'
}, 1000);

When updating data, there are a few points to note:

  1. When accessing state.address, get will be triggered, and track will also be triggered. Since the effect has been executed during the update, there is currently no effect in the active state, so it will not be repeated here. Collect dependencies.

  2. The res returned when accessing state.address is an object. At this time, reactive will be triggered again, but the return value at this time already exists in reactiveMap, so responsive processing will not be repeated.

  3. set will be triggered when updating. At this time, if the new value is different from the old value, trigger will be triggered, and the child item depsMap of targetMap will be triggered. Get the corresponding effect function from and execute it.

effect(() => {<!-- -->
  app.innerHTML = state.address.city
})

When executing effect again, there are several points to note:

  1. Accessing state.address again will also trigger get. The difference from the first time is that the data at this time has been updated to the latest value. At the same time, activeEffect will be assigned again.

  2. Since the relevant values have been cached during previous accesses, dependency collection and responsive processing will not be repeated when accessed again, but the latest values will be returned directly.

Summary

To sum up, the responsive principle of vue3 realizes efficient data change tracking and automatic update mechanism by using Proxy object to implement data proxy, combined with side effect functions and dependency tracking. . This design makes vue3 more flexible and efficient in handling the relationship between data and views.