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:
-
If an object has been created as a reactive object, the reactive object is returned directly to avoid repeatedly proxying the same target object.
-
Using
WeakMap
can avoid memory leak problems, because when the target objecttarget
is no longer referenced, its corresponding proxy objectexistingProxy
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:
-
Performance optimization: The proxy will only be triggered when needed, avoiding unnecessary proxy and responsive data updates, and improving the performance of component rendering.
-
Reduce unnecessary memory overhead: Responsive data objects will only be created when needed, avoiding unnecessary memory overhead.
-
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:
-
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 noeffect
in the active state, so it will not be repeated here. Collect dependencies. -
The
res
returned when accessingstate.address
is an object. At this time,reactive
will be triggered again, but the return value at this time already exists inreactiveMap
, so responsive processing will not be repeated. -
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 itemdepsMap of
and execute it.targetMap
will be triggered. Get the correspondingeffect
function from
effect(() => {<!-- --> app.innerHTML = state.address.city })
When executing effect
again, there are several points to note:
-
Accessing
state.address
again will also triggerget
. 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. -
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.