Vue2. X uses the Object.defineProperty method to detect Object attribute changes, but this method has some inherent defects:

  1. Poor performance;
  2. [Fixed] New attributes on objects cannot be detected
  3. Changing the length property of an array cannot be detected.

Vue3.0 is a big refactoring. Source code is written in TypeScript, and more than 98% of current code is written in TypeScript. Data reactivity uses ES6 Proxy instead of Object.defineProperty, which has better performance, and arrays, like objects, can directly trigger GET and set methods. A Proxy, called a Proxy, is a wrapper that intercepts and changes the operations of the underlying javascript engine.

Call new Proxy(target, handler) to create a Proxy from a target object. The Proxy intercepts low-level object operations on the target inside the javascript engine. These low-level operations will trigger trap functions in response to specific operations. You need to pass in two arguments, target for the target object and Handler for the handler object that contains the trap function.

The following uses Proxy to simulate a responsive system that implements VUe3.0.

Object proxy

// Create object responsive core methods
function reactive(target){
    // If target is not an object, return it directly
    if(target === null || typeoftarget ! = ='object') return target;

    return new Proxy(target, {
        get(target, property, receiver) {
            console.log('Get value')
            const result = Reflect.get(target, property, receiver);
            return result;
        },
        set(target, property, value, receiver) {
            console.log('Set value')
            const result = Reflect.set(target, property, value, receiver);
            return result;
        },
        deleteProperty(target, property) {
            return Reflect.deleteProperty(target, property); }}); }Copy the code

Reflect is a built-in object that provides methods to intercept javascript operations. Each proxy trap corresponds to a Reflect method with the same name and parameters.

const obj = {name: 'icon'};
const proxy = reactive(obj);
proxy.name = 'lee';
console.log(proxy.name);
Copy the code

The result is as follows

This is the initial implementation of data object proxy, but this is not able to detect multi-layer objects. So we need to make a judgment on the return value in the get trap function. If the return value is an object, the return value also creates a proxy object, that is, a recursive call.

function reactive(target){...get(target, property, receiver) {
        console.log('Get value')
        const result = Reflect.get(target, property, receiver);
        // Add multi-layer object judgment
        if(target ! = =null || typeof target === 'object') return reactive(result);
        returnresult; },... }Copy the code
const obj = {name: 'icon'.address: {province: 'Guangdong'}};
const proxy = reactive(obj);
proxy.address.province = 'Beijing';
Copy the code

The result is as follows

This solved the problem of undetectable multilayer objects, but then there was a new problem, such as

  • Multiple proxies are performed on the unified target object
  • The target object is proxied and the proxy object is proxied

These two cases are meaningless. Next, we will use WeakMap data structure to solve the problem. Of course, there are other solutions, it depends on you.

// key: target object points to value: proxy object
const toProxy = new WeakMap(a);// key: the proxy object points to value: the target object
const toRaw = new WeakMap(a);function reactive(target){...If the target object already has a proxy object, the proxy object is returned directly
    const proxy = toProxy.get(target);
    if(proxy) return proxy;
    
    // If the target object is a proxy object and there is a corresponding real object, the object is returned directly
    if(toRaw.get(target)) return target;
    
    // Instead of returning the proxy object directly, it needs to be stored
    const observerd = new Proxy(target, {
        ...
    });
    
    toProxy.set(target, observerd);
    toRaw.set(observerd, target);
    
    // Finally return the proxy object
    return observerd;
}
Copy the code

Do you think this is perfect?

Of course not. If you do an array push, you’ll see that the get and set trap is executed twice.

Because the push method changes the array’s length as it adds elements to the array, the trap function fires twice, once by incrementing the array index 3 to 4 and once by changing the array’s length attribute to 4. This way, if you update the view in the set trap function, it will be updated twice.

Next we add the relevant judgments about whether attributes are new or modified

.const observerd = new Proxy(target, {
        ...
        
        set(target, property, value, receiver) {
            console.log('Set value')
            const oldValue = Reflect.get(target, property);
            const result = Reflect.set(target, property, value, receiver);
            // Determine whether the current object has the specified attribute
            if(! target.hasOwnProperty(property)) {console.log('New Properties')}else if(oldValue ! == value) {console.log('Modify Properties')}returnresult; },... }); .Copy the code

Ok, so that’s the end of the data object proxy.

Depend on the collection

Next comes the tricky collection of dependencies in Vue3.0, which uses the effect function to wrap dependencies, called side effects. The effect function is simulated as follows.

.// Save the effect array as a stack
const effectStack = [];

function effect(fn) {
    // Create a reactive effect
    const effect = createReactiveEffect(fn);
    // Effect is executed once by default, essentially calling the incoming fn function
    effect();
}

// Create a reactive effect function
function createReactiveEffect(fn) {
    // response effect
    const effect = function() {
        try {
            // Save effect to the global effectStack
            effectStack.push(effect);
            return fn();
        } finally {
            // After invoking the dependency, effect pops upeffectStack.pop(); }}return effect;
}
Copy the code
const proxy = reactive({name: 'icon'});
effect(() = > {
    console.log(proxy.name);
});
proxy.name = 'lee';
Copy the code

The results

You can see from the output that effect is not executed when the name property changes, except for the default effect, which is executed once. In order to make effect execute again when the attribute of the object changes, it is necessary to associate the attribute of the object with effect, which can be stored by Map. Considering that an attribute may be associated with multiple dependencies, the key stored in the mapping relationship should be the object attribute. Value is the Set object that holds all effects. Set is chosen instead of array because Set has the effect of de-duplicating. In addition, the attribute is the attribute of the object after all, and cannot exist independently without the object. To track the dependency of different object attributes, a WeakMap is also needed, where key is the object itself and value is the Map that saves all the attributes and dependencies.

With the data structure defined, we can next write a dependency collection function, Track.

.// Store the Map of the object and its property dependencies. Key is the object and value is the Map
const targetMap = new WeakMap(a);// Trace dependencies
function track(target, property) {
    // Get dependencies in the effectStack
    const effect = effectStack[effectStack.length - 1];
    
    // If there is a dependency
    if(effect) {
        // Retrieve the Map corresponding to this object
        let depsMap = targetMap.get(target);
        // If it does not exist, the target object is the key, the new Map is the value, and the new Map is saved in targetMap
        if(! depsMap) { targetMap.set(target, depsMap =new Map()}// Fetch all effects corresponding to this property from the Map
        let deps = depsMap.get(property);
        // If it does not exist, the attribute is the key, the new set is the value, and the depsMap is stored
        if(! deps) { depsMap.set(property, deps =new Set());
        }
        // Determine if effect already exists in Set, if not, add it to deps
        if(! deps.has(effect)) { deps.add(effect); }}}Copy the code

Next, all effects that trigger the property association are executed when the property changes, so we’ll write another trigger function

.// Perform all effects associated with the property
function trigger(target, type, property) {
    const depsMap = targetMap.get(target);
    if(depsMap) {
        let deps = depsMap.get(property);
        // All effects associated with the current property are executed in sequence
        if(deps) {
            deps.forEach(effect= >effect()); }}}Copy the code

With the dependency collection function and the dependency triggering function both OK, it’s natural to need to perform the collection and triggering dependencies somewhere. The dependency collection is in the GET trap function, and the triggering dependency is in the set trap function when the property changes.

Add dependency collection and triggering to Proxy

Improve the code

// key: target object points to value: proxy object
const toProxy = new WeakMap(a);// key: the proxy object points to value: the target object
const toRaw = new WeakMap(a);function reactive(target){
    // If target is not an object, return it directly
    if(target === null || typeoftarget ! = ='object') return target;
    
    If the target object already has a proxy object, the proxy object is returned directly
    const proxy = toProxy.get(target);
    if(proxy) return proxy;
    
    // If the target object is a proxy object and there is a corresponding real object, the object is returned directly
    if(toRaw.get(target)) return target;
    
    / / proxy agent
    const observerd = new Proxy(target, {
        get(target, property, receiver) {
            console.log('Get value')
            const result = Reflect.get(target, property, receiver);
            // Rely on collection
            track(target, property);
            if(target ! = =null || typeof target === 'object') return reactive(result);
            return result;
        },
        set(target, property, value, receiver) {
            console.log('Set value')
            const oldValue = Reflect.get(target, property);
            const result = Reflect.set(target, property, value, receiver);
            // Determine whether the current object has the specified attribute
            if(! target.hasOwnProperty(property)) {console.log('New Properties')
                trigger(target, 'add', property);
            }else if(oldValue ! == value) {console.log('Modify Properties')
                trigger(target, 'set', property);
            }
            return result;
        },
        deleteProperty(target, property) {
            return Reflect.deleteProperty(target, property); }}); toProxy.set(target, observerd); toRaw.set(observerd, target);// Finally return the proxy object
    return observerd;
}

// Save the effect array as a stack
const effectStack = [];

function effect(fn) {
    // Create a reactive effect
    const effect = createReactiveEffect(fn);
    // Effect is executed once by default, essentially calling the incoming fn function
    effect();
}

// Create a reactive effect function
function createReactiveEffect(fn) {
    // response effect
    const effect = function() {
        try {
            // Save effect to the global effectStack
            effectStack.push(effect);
            return fn();
        } finally {
            // After invoking the dependency, effect pops upeffectStack.pop(); }}return effect;
}

// Store the Map of the object and its property dependencies. Key is the object and value is the Map
const targetMap = new WeakMap(a);// Trace dependencies
function track(target, property) {
    // Get dependencies in the effectStack
    const effect = effectStack[effectStack.length - 1];
    
    // If there is a dependency
    if(effect) {
        // Retrieve the Map corresponding to this object
        let depsMap = targetMap.get(target);
        // If it does not exist, the target object is the key, the new Map is the value, and the new Map is saved in targetMap
        if(! depsMap) { targetMap.set(target, depsMap =new Map()}// Fetch all effects corresponding to this property from the Map
        let deps = depsMap.get(property);
        // If it does not exist, the attribute is the key, the new set is the value, and the depsMap is stored
        if(! deps) { depsMap.set(property, deps =new Set());
        }
        // Determine if effect already exists in Set, if not, add it to deps
        if(! deps.has(effect)) { deps.add(effect); }}}// Perform all effects associated with the property
function trigger(target, type, property) {
    const depsMap = targetMap.get(target);
    if(depsMap) {
        let deps = depsMap.get(property);
        // All effects associated with the current property are executed in sequence
        if(deps) {
            deps.forEach(effect= >effect()); }}}Copy the code

So far, we have written all the code to simulate and implement vue3.0 responsiveness. Of course, vue3.0’s responsive API is not only reactive, but also includes REF, computed and Watch, and its internal principles are similar.