Vue3 has a breakthrough implementation of its responsiveness, so how different is vuE3’s responsiveness from VUE2?

Object. DefineProperty

I’m sure you already know that vue2’s reactive principle is based on object.defineProperty. The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object and returns the Object

DefineReactive (obj, key, val); this.$set(obj, key, val)

// vue.util.defineReactive(obj, key, val)
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      console.log("get key:", key, val);
      return val;
    },
    set(v) {
      console.log("set key:", key, v); val = v; }}); }// this.$set(obj, key, val)
function set(obj, key, val) {
  defineReactive(obj, key, val);
}

let obj = {};
defineReactive(obj, "name"."coboy");
// read it
obj.name;
// Make a change
obj.name = "coman";
Copy the code

So you can see we’ve implemented key interception but now we need to do listening interception manually, we need to do automatic interception

function observe(obj) {
  if (typeofobj ! = ="object" || obj === null) {
    return;
  }
  Object.keys(obj).forEach((key) = > defineReactive(obj, key, obj[key]));
}
Copy the code
function defineReactive(obj, key, val) {
  observe(val);
  Object.defineProperty(obj, key, {
    get() {
      console.log("get key:", key, val);
      return val;
    },
    set(v) {
      console.log("set key:", key, v);
      observe(v); // It is possible to set an objectval = v; }}); }Copy the code

And then we can

let obj = { name: "coboy".age: 18.info: { desc: "Technophiles"}}; observe(obj);// read it
obj.name;
// Make a change
obj.name = "coman";
// Depth read
obj.info.desc;
// Make a change
obj.info.desc = "Long live the great Unity of the peoples of the world.";
Copy the code

Vue2 response formula principle

The principle of analysis

During initialization, Observer response processing is performed on data data, Compile parse and Compile template, and generate update function updater. In this process, Watcher is generated. When the corresponding data changes in the future, Watcher will call the update function and establish a relationship between Dep and the key that responds to the data. Once the data changes in the future, the corresponding Dep will be found first and all Watcher will be informed to perform the update function

  • Vue: frame constructor
  • Observer: Perform data reactivity (distinguish whether data is an object or an array)
  • Compile: Compile templates, initialize views, collect dependencies (update functions, create by Watcher)
  • Watcher: Execute update function (update DOM)
  • Dep: Manage multiple Watcher, batch update

We will only delve into the reactive principles today, leaving out the rest of vUE’s framework

Implement a vue2 simplest dependency collection update example

Analog update function
<div id="app"></div>
Copy the code
let obj = { name: "coboy".age: 18.info: { desc: "Technophiles"}}; observe(obj);// simulate the update function
function update(key) {
    const fn = function (val) {
        app.innerHTML = val
    }
    fn(obj[key])
}

update('age')
Copy the code

Introducing dynamic updates

setInterval(() = > {
    obj.age++
}, 1000)
Copy the code

Find that the get and set methods in defineReactive have been intercepted, but the mock update function cannot update the HTML yet

Rely on collection and triggering principles

This brings us to two of the most important concepts in Watcher, Dep, and VUe2

// Update specific nodes
class Watcher {
    constructor(key, update) {
        this.key = key
        this.updater = update
    }

    update() {
        const val = obj[this.key]
        this.updater(val)
    }
}
// There is a one-to-one correspondence between Dep and the responsive attribute key
class Dep {
    constructor() {
        // All followers are saved in Dep
        this.deps = []
    }
	/ / subscribe
    subscribe(dep) {
        this.deps.push(dep)
    }
	/ / notice
    notify() {
        this.deps.forEach(dep= > dep.update())
    }
}
Copy the code

Watcher is responsible for updating specific nodes

Watcher has to know which key is associated with which node is associated with Watcher, who AM I updating, and that’s what Watcher does, just to do things, no matter who it’s for. Update when Dep notifies you

There is a one-to-one correspondence between the Dep and the responsive property key

The Dep is like the butler, telling people what to do and telling watchers to update

Generate Watcher in the update function

// simulate the update function
function update(key) {
    const fn = function (val) {
        app.innerHTML = val
    }
    fn(obj[key])
    new Watcher(key, function(val) {
        fn(val)
    })
}
Copy the code

Depend on the collection

Dependency collection is a publish-subscribe process that links the update function that contains Watcher to the Dep

So where is the DEP created? We know that each key has a DEP for it, so we should generate a DEP every time we perform defineReactive

The closure will form a key and a unique DEP, so the deP and the key will form a one-to-one relationship, one for each callback, and then the relationship between watcher and deP will be established in the user get value, that is, in the property interception GET. This is called dependency collection.

Triggering dependency collection

Set a global static property to store the Watcher instance

Read the get method in triggering Object.defineProperty for dependency collection

Once the relationship is established, it is set to null to prevent it from being added frequently

Check if target exists and subscribe if it does

Triggered update

Dep notifies future key updates

A view uses a key from data, which is called a dependency. The same key may appear multiple times, and each time they need to be collected and maintained with a Watcher, a process called dependency collection. Multiple Watchers require a Dep to manage and are notified uniformly when updates are needed.

The responsivity principle of VUE3

proxy

Proxy objects are used to create a Proxy for an object to intercept and customize basic operations (such as property lookup, assignment, enumeration, function calls, and so on).

Implement a simple reactive
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            console.log('get key:', key, target[key])
            return target[key]
        },
        set(target, key, val) {
            target[key] = val
            console.log('set key:', key, target[key])
        }
    })
}
const obj = reactive({ name: "coboy".age: 18.info: { desc: "Technophiles" } })
obj.name / / read
obj.name = 'coman' // Make a change
Copy the code

We saw that data interception can also be implemented as with vue2’s Object.defineProperty

But it can’t read objects that are nested

obj.info.desc // Cannot read
Copy the code
Proxy is a lazy Proxy

We find that the Proxy is a lazy Proxy. It doesn’t even process the specific key if you don’t access it, so it needs to do recursive interception in get

One thing to do during initialization is to rely on collection to inform components or pages of future updates when these values change. This is the same as VUe2, but the implementation is completely different. Vue3 has a breakthrough implementation. Vue2 is to create a Watcher, a DEP to establish a relationship

Vue3 new API – effect

Effect will receive a function fn, which will establish a relationship with its internal dependent response data.

// Temporarily store side effect functions
const effectStack = [];

// Rely on the collection function
function effect(fn) {
    effectStack.push(fn)
    fn() // Execute track immediately, and the component needs to be initialized
}

// Dependency collection: establish the mapping between target,key, and FN
function track(target, key) {}// Trigger update When a responsive data changes, get the corresponding FN based on target and key and execute them
function trigger(target, key) {}Copy the code

First build a framework according to the source code, and then write in detail

The received FN function may fail when executed, so a higher-order function createReactiveEffect is created to handle this, so the fn function passed in by the user is wrapped, because there are a lot of extra things to do.

Since fn is executed, reactive data may exist in FN, so get and set functions in Proxy will be triggered

If an FN function reads a value, the relationship between the current target, key, and FN must be established. If a key value changes in the future, the related FN function must be notified and executed again

Track,trigger function implementation
const targetMap = new WeakMap(a)// WeakMap weak reference, does not affect garbage collection mechanism

// Dependency collection: Establish the mapping between target/key and FN
function track(target, key) {
    // 1. Get the current side effect function
    const effect = effectStack[effectStack.length - 1]
    if(effect){
        // 2. Retrieve the map corresponding to the target/key
        let depMap = targetMap.get(target)
        if(! depMap){ depMap =new Map()
           targetMap.set(target, depMap) 
        }

        // 3. Obtain the set corresponding to the key
        let deps = depMap.get(key)
        if(! deps){ deps =new Set()
            depMap.set(key, deps)
        }

        // 4
        deps.add(effect)
    }
}

// Trigger update: When a responsive data changes, get the corresponding FN according to target and key and execute them
function trigger(target, key) {console.log('targetMap', targetMap)
    // 1. Get the set corresponding to target/key and iterate through them
    const depMap = targetMap.get(target)

    if(depMap){
        const deps = depMap.get(key)
        if(deps){
            deps.forEach(dep= >dep()); }}}Copy the code

So let’s test that out

const obj = reactive({ name: "coboy".age: 18.info: { desc: "Technophiles" } })

effect(() = > {
    console.log('effect1', obj.name)
})
effect(() = > {
    console.log('effect2', obj.name, obj.info.desc)
})
obj.name = 'coman' // Make a change
obj.info.desc = 'coder' // Make a change
Copy the code

This function is associated with the name key, and if the value of name changes in the future, this function will be executed again,

The first time, both functions are executed immediately, printing effect1, obj. Name, coman, and obj. This key has an associated effect1, and effect2 has re-executed the obj.info.desc value to coder. Since only effect2 is associated with obj.info.desc, only effect2 will be executed

Comparison of response formula principle between VUE2 and VUe3

Add a dependency function fn to effect. If fn’s response data changes, the dependency function of fn will be executed again. If effect adds a component update function, then the dependency variables in that component can also be mapped by the component update function. So effect is all about preserving the mapping between the two.

If Effect adds a component update function fn, this update function is wrapped into a new higher-order function fN1, which executes immediately, a process called component initialization. In this process, dependency mapping is also saved. When the component initializes and reads the value of the reactive data, it triggers the getter function in Proxy, and then uses the track method to collect the dependency, and establishes the dependency relationship between the current key and the function fn.

Fn is temporarily stored in a global variable effectStack before execution. When track is triggered to execute, fn function in effectStack can be taken out, and then the current responsive object target and key are stored in a WeakMap data structure. The corresponding relationship is that the responsive object target is a WeakMap,key is a Map, and FN is a Set. Because a key may be called by multiple FN, and FN does not need to be added repeatedly, so the Set data structure needs to be used.

If the reactive data changes in the future, the Proxy setter function will be triggered, which will trigger the update function trigger, Trigger retrieves the fn update function corresponding to the current target and key from the data store just saved in the getter and executes it again.

Vue2 intercepts the key of each responsive Object using Object.defineProperty to detect data changes, which is obviously not a very good approach.

  1. First of all, vue3 only supports objects well, but not arrays, which require unique processing. Vue3 uses Proxy to Proxy the whole object, and arrays can also detect changes in data
  2. Vue2 needs to recursively initialize all the keys of the variable object, which is slow. If the initialized object is very large, the cost of time and memory is very high, because many DEPs and Watchers need to be created during initialization to store the dependency
  3. Vue2 new and deleted attributes cannot be monitored, requiring a special API
  4. Vue2 does not support data structures such as Map,Set, and Class

Vue3 uses a Proxy, and Proxy is a lazy mechanism. If you don’t access specific values, you won’t produce dependent variables and functions. Here’s a good example: Proxy is equivalent to putting data in a community, Proxy puts a security guard at the door to intercept, but as long as you do not go out of the community, the security guards will not intercept you, while VUe2 is equivalent to sending a lot of security guards to conduct door-to-door inspection, that is, how many residents in this community, how many times to check.

All the code

<! DOCTYPEhtml>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <title>Handwriting to achieve vuE2 and VUe3 responsive and dependent collection, trigger comparison</title>
</head>
<body>
    <div id="app"></div>
    <script>
        // vue.util.defineReactive(obj, key, val)
        function defineReactive(obj, key, val) {
            observe(val);
            const dep = new Dep()
            Object.defineProperty(obj, key, {
                get() {
                    console.log("get key:", key, val);
                    Dep.target && dep.subscribe(Dep.target) 
                    return val;
                },
                set(v) {
                    console.log("set key:", key, v);
                    observe(v); // It is possible to set an object
                    val = v;
                    // Notification update
                    dep.notify()
                },
            });
        }

        function observe(obj) {
            if (typeofobj ! = ="object" || obj === null) {
                return;
            }
            Object.keys(obj).forEach((key) = > defineReactive(obj, key, obj[key]));
        }
        // this.$set(obj, key, val)
        function set(obj, key, val) {
            defineReactive(obj, key, val);
        }

        class Watcher {
            constructor(key, update) {
                this.key = key
                this.updater = update
                // Triggers dependency collection
                Dep.target = this // Set a global static property to store the Watcher instance
                obj[key] // Read the get method that triggers Object.defineProperty for dependency collection
                Dep.target = null // Once the relationship is established, set it to null to prevent it from being added frequently
            }

            update() {
                const val = obj[this.key]
                this.updater(val)
            }
        }
        // Each key is associated with a Dep
        class Dep {
            constructor() {
                this.deps = []
            }

            subscribe(dep) {
                this.deps.push(dep)
            }

            notify() {
                this.deps.forEach(dep= > dep.update())
            }
        }

        // simulate the update function
        function update(key) {
            const fn = function (val) {
                app.innerHTML = val
            }
            fn(obj[key])
            new Watcher(key, function(val) {
                fn(val)
            })
        }

        let obj = { name: "coboy".age: 18.info: { desc: "Technophiles"}}; observe(obj); update('age')
        
        setInterval(() = > {
            obj.age++
        }, 1000)
</script>
</body>
</html>
Copy the code
function reactive(obj) {
    return new Proxy(obj, {
        get(target, key) {
            console.log('get', key)
            // Rely on collection
            track(target, key)
            return typeof target[key] === 'object' ? reactive(target[key]) : target[key]
        },
        set(target, key, val) {
            console.log('set', key)
            target[key] = val
            // Notification update
            trigger(target, key)
        },
        deleteProperty(target, key) {
            console.log('delete', key)
            delete target[key]
            trigger(target, key)
        }
    })
}

// Temporarily store the side effect function
const effectStack = [] // Why array? Because effects are nested, there are multiple side effects functions

// Rely on the collection function: wrap fn, execute fn immediately, return the result of the wrap
function effect(fn) {
    const e = createReactiveEffect(fn)
    e()
    return e
}

function createReactiveEffect(fn) {
    const effect = function() {
        try{
            effectStack.push(fn)
             return fn() // Execution may return results, so return
        } finally { 
            effectStack.pop()
        }
    }
    return effect
}

// Save the dependency data structure
const targetMap = new WeakMap(a)// WeakMap weak reference, does not affect garbage collection mechanism

// Dependency collection: Establish the mapping between target/key and FN
function track(target, key) {
    // 1. Get the current side effect function
    const effect = effectStack[effectStack.length - 1]
    if(effect){
        // 2. Retrieve the map corresponding to the target/key
        let depMap = targetMap.get(target)
        if(! depMap){ depMap =new Map()
           targetMap.set(target, depMap) 
        }

        // 3. Obtain the set corresponding to the key
        let deps = depMap.get(key)
        if(! deps){ deps =new Set()
            depMap.set(key, deps)
        }

        // 4
        deps.add(effect)
    }
}

// Trigger update: When a responsive data changes, get the corresponding FN according to target and key and execute them
function trigger(target, key) {
    // 1. Get the set corresponding to target/key and iterate through them
    const depMap = targetMap.get(target)

    if(depMap){
        const deps = depMap.get(key)
        if(deps){
            deps.forEach(dep= >dep()); }}}const obj = reactive({ name: "coboy".age: 18.info: { desc: "Technophiles" } })

effect(() = > {
    console.log('effect1', obj.name)
})
effect(() = > {
    console.log('effect2', obj.name, obj.info.desc)
})
obj.name = 'coman' // Make a change
obj.info.desc = 'coder' // Make a change

Copy the code