Implementation Principle of Vue3's Reactive System
Description: Vue3's reactive system is one of its core features, capable of automatically tracking data changes and updating related views. Unlike Vue2's implementation based on Object.defineProperty, Vue3 uses ES6's Proxy and Reflect API to build the reactive system. Understanding its principle is key to mastering the Vue3 framework.
Problem-Solving Process:
-
Core Goal and Basic Idea
- Goal: When a property of data (a JavaScript object) is read, we need to record "who" read it (this process is called dependency collection). When a property of data is modified, we need to notify all places that have "read" it to update (this process is called triggering updates).
- Idea: Vue3 uses
Proxyto "proxy" a plain object.Proxycan intercept (or "trap") various fundamental operations on the target object, such as getting a property (get), setting a property (set), deleting a property (deleteProperty), etc. This way, we can execute custom logic when these operations occur, thereby achieving dependency collection and triggering updates.
-
Key Players: effect and reactive
reactivefunction: This is the function used in Vue3 to create reactive objects. It takes a plain object as a parameter and returns a Proxy of that object.effectfunction: This is the "side effect" function of the reactive system. It takes a function (let's call itfn) as a parameter.effectwill immediately executefnonce. The key point is that whenfnreads a property of a reactive object during execution, Vue can establish a connection betweenfn(the side effect) and this property (the dependency).
-
Step-by-Step Implementation Principle
-
Step One: Create the Simplest reactive Function
First, create a function that returns a Proxy. This proxy temporarily only interceptsgetandsetoperations.function reactive(target) { return new Proxy(target, { get(obj, key) { // Intercept the "get" operation console.log(`Read property ${key}: ${obj[key]}`); return obj[key]; // Return the original value }, set(obj, key, value) { // Intercept the "set" operation console.log(`Modified property ${key}: from ${obj[key]} to ${value}`); obj[key] = value; // Set the original value return true; // Indicate success } }); } // Test const state = reactive({ count: 0 }); state.count; // Console output: "Read property count: 0" state.count = 1; // Console output: "Modified property count: from 0 to 1"Now we can intercept basic operations on the object.
-
Step Two: Introduce effect and Dependency Collection (Core)
Now we need a mechanism to record "which effect is currently running" and "which effects depend on which property of which reactive object".activeEffectglobal variable: Used to store the currently executingeffectfunction.targetMapglobal WeakMap: Used to build the dependency relationship tree.- Key: The reactive object (
target). - Value: A Map.
- The key of this Map is the property name (
key) of the reactive object. - The value is a Set, storing all
effectfunctions that depend on thistarget.key.
- The key of this Map is the property name (
- Key: The reactive object (
let activeEffect = null; // Current active effect const targetMap = new WeakMap(); // Dependency storage warehouse function track(target, key) { // Dependency collection function, called inside the get interceptor if (!activeEffect) return; // No collection needed if not read inside an effect // 1. Get the depsMap corresponding to the current target from targetMap let depsMap = targetMap.get(target); if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } // 2. Get the dep (a Set) corresponding to the current key from depsMap let dep = depsMap.get(key); if (!dep) { dep = new Set(); depsMap.set(key, dep); } // 3. Add the current active effect to the dep dep.add(activeEffect); console.log(`Tracked dependency: ${key} -> current effect`); } function trigger(target, key) { // Trigger update function, called inside the set interceptor const depsMap = targetMap.get(target); if (!depsMap) return; const dep = depsMap.get(key); if (dep) { // Iterate through the dep set, execute all effect functions inside dep.forEach(effect => effect()); console.log(`Triggered update: ${key}`); } } function effect(fn) { // Wrap the passed function into an effect activeEffect = fn; // Set as the current active effect fn(); // Execute the function immediately. During execution, get will be triggered, thus collecting dependencies. activeEffect = null; // Reset after execution } // Update the reactive function function reactive(target) { return new Proxy(target, { get(obj, key) { track(obj, key); // Collect dependencies on read return obj[key]; }, set(obj, key, value) { obj[key] = value; trigger(obj, key); // Trigger updates on set return true; } }); } -
Step Three: Complete Flow Demonstration
Let's use a complete example to connect the entire flow.// Create a reactive object const user = reactive({ name: 'Alice', age: 25 }); // Define an effect effect(() => { // This function executes immediately console.log(`Username is: ${user.name}`); }); // Console immediately outputs: "Username is: Alice" // Meanwhile, during effect execution, user.name is read, // triggering the get interceptor and calling the track function. // At this point, the following dependency is established in targetMap: // targetMap: { user -> depsMap } // depsMap: { 'name' -> Set( [current effect function] ) } // Modify data to trigger an update user.name = 'Bob'; // 1. Trigger the set interceptor // 2. Call trigger(user, 'name') // 3. trigger finds the dep (a Set) corresponding to user.name from targetMap // 4. Iterate through this Set, executing all effect functions inside // 5. Console outputs again: "Username is: Bob"
-
-
Vue3's Actual Implementation and Optimizations
The above is the core principle. The implementation in Vue3's source code (in the@vue/reactivitypackage) is more complex and robust, mainly with the following optimizations:- Nested effects: Supports component nesting, managed via an
effectstack. - Scheduler: Allows controlling the execution timing after
triggeris called, e.g., placing it in a microtask queue to achieve batch asynchronous updates, which is the basis of nextTick's principle. - Avoiding duplicate collection: Ensures the same effect is not collected repeatedly in the same property's dep.
- Cleanup for branch switching: For example, in
v-if, when conditions change, dependencies that are no longer needed need to be cleaned up. - Ref & Computed: Based on the same reactive core,
refsolves reactivity for primitive types using the.valueproperty, andcomputedis a specialeffectwith lazy calculation and cached value properties.
- Nested effects: Supports component nesting, managed via an
Summary: Vue3's reactive system proxies objects via Proxy, calls track for dependency collection during get operations, and calls trigger to trigger updates during set operations. The effect function is key to building the bridge between reactive data and side effect functions. The entire system revolves around a global targetMap (WeakMap) to maintain all dependency relationships.