Implementation principle of Vue responsive data (storage and execution process of handwritten side effect functions)

1. Imperative and declarative frameworks

Imperative frameworks focus on process

The declarative framework focuses on results (the bottom layer encapsulates imperative DOM acquisition and modification)

2.vue2 Object.defineProperty() implementation of two-way binding

<body>
    <div id="app">
        <input type="text" />
        <h1></h1>
        <button>Button</button>
    </div>
</body>
<script>
        // vue2 implements double binding
        const input = document.getElementsByTagName('input')[0]
        const h1 = document.getElementsByTagName('h1')[0]
        const btn = document.getElementsByTagName('button')[0]
        let data = { text: '' }
        //Input data into the input box, h1 data and text are consistent; when the button is clicked, the h1 label data and input box data change at the same time
        Object.defineProperty(data, 'text', {
            get() {
                return data['text'];
            },
            set(value) {
                //After obtaining the value, set the content after h1 to text
                h1.innerText = value;
                input.value = value;
                return true;
            }
        });

        input.oninput = function (e) {
            data.text = e.target.value;
        }

        btn.onclick = function () {
            data.text = "Hello"
        }
</script>

3. The same page Vu3 implements new Proxy()

 // vue3 implements double binding
        const input = document.getElementsByTagName('input')[0]
        const h1 = document.getElementsByTagName('h1')[0]
        const btn = document.getElementsByTagName('button')[0]
        let data = { text: '' }

        let obj = new Proxy(data, {
            get(target, property) {
                return target[property]
            },
            set(target, property, value) {
                h1.innerText = value;
                input.value = value;
                return true;
            }
        })

        input.oninput = function (e) {
            obj.text = e.target.value;
        }

        btn.onclick = function () {
            obj.text = "Hello"
        }

4. Basic implementation of responsive data

The key to responsive data is to intercept the setting and reading operations of object properties

const data = { text: '' }
function effect () {
    document.body.innerText = data.text
}

5. The difference between vue2 and vue3 responsive data implementation

  • Implementation of vue2: When you pass an ordinary JavaScript object into a Vue instance as the data option, Vue will traverse all the properties of this object and use Object.defineProperty() to convert all these properties into getters. /setter.
  • Implementation of vue3: When we return a normal JavaScript object from a component’s data function, Vue will wrap the object in a Proxy with get and set handlers

6.Vue3 proxy’s simple implementation of responsive data interception

//Initial data
const data = { text: '' }
// Bucket to store side effect functions
const bucket = new Set()
//Proxy the data
const obj = new Proxy(data, {
    get(target, key) {
        bucket.add(effect)
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true
    }
})

function effect () {
    document.body.innerText = obj.text
}

effect()

setTimeout(() => {
    obj.text = 'Hello'
}, 1000)

7. Problems in simple implementation

Think about the problem with this piece of code.

  • 1. The name of the side effect function is hard-coded
  • 2. There is no clear relationship between the side-effect function and the target field.

The name of the side effect function is hard-coded – Solution:

Problem with hard-coded names: In real situations, it is impossible to have only one side-effect function. If there are multiple functions, each function will call a side-effect function. For example, setting obj.a = 2 will also call the bucket.forEach(fn => fn()) method in the set method.

A common side effect function, the function that performs DOM modification is passed into the side effect function in the form of a closure (callback function), so that the same function is not returned every time

let activeEffect

function effect(fn) {
    activeEffect = fn
    fn()
}

effect(() => {
    document.body.innerText = obj.text
})

const obj = new Proxy(data, {
    get(target, key) {
        if (activeEffect) {
            bucket.add(activeEffect)
        }
        return target[key]
    },
    set(target, key, newVal) {
        target[key] = newVal
        bucket.forEach(fn => fn())
        return true
    }
})

There is no clear relationship between the side effect function and the target field – Solution:

The above code binds the same side effect function to each attribute. In fact, when it is actually needed, when text is modified, its own function is called, and when a is modified, the function corresponding to a is called.

solve:

  1. Use Map key-value data structure (divided into two layers) to store side effect functions, and each data object corresponds to a map key;
  2. Different attributes under a data object are stored under a Map data, and the value of this map is stored as a side effect function (stored in Set form)
  3. When using it, you can obtain the set data (side effect function for each attribute operation) of the value of the corresponding attribute key (the key of each object) under the object’s map (data object) and perform traversal execution.

const obj = new Proxy(data, {
    get(target, key) {
        console.log(activeEffect, 'activeEffect')
        // No side-effect function, fault-tolerant processing, return directly
        if (!activeEffect) return target[key]
        // Determine whether the association between key and target already exists in the bucket
        let depsMap = bucket.get(target)
            //Create a new Map structure associated with target
        if (!depsMap) {
            bucket.set(target, (depsMap = new Map()))
        }
        // Determine whether there is a relationship between key and effect in the current Map data
        let deps = depsMap.get(key)
        // If it does not exist, create a new Set associated with the key.
        if (!deps) {
            depsMap.set(key, (deps = new Set()))
        }
        //Finally add the currently activated side effect function to the bucket
        deps.add(activeEffect)
        return target[key]
    },
    set (target, key, newVal) {
        target[key] = newVal

        // Get the corresponding data under the bucket
        const depsMap = bucket.get(target)
        if (!depsMap) return
        // Get the execution function of side effects based on key
        const effects = depsMap.get(key)
        //Execute side effect function
        effects & amp; & amp; effects.forEach(fn => fn())
    }
})

effect(() => {
    document.body.innerText = obj.text
})
effect(() => {
    document.title = obj.a
})

8. Full implementation – my test

 const data = { text: 'This is obj.title', a:'The implementation principle of vue responsive data' }
        // Bucket to store side effect functions
        const bucket = new Map()

        //Variables to store side effect functions
        let activeEffect;

        //Proxy the data
        const obj = new Proxy(data, {
            get(target, key) {
                // Set the side effect function to the bucket of map data
                // Determine if activeEffect does not exist and return directly
                if (!activeEffect) return target[key];

                // Set the side effect function to the bucket when activeEffect exists
                let targetMap = bucket.get(target); //The existence of the target object can determine whether the key exists (targetMap is both the value of the bucket targetMap = new Map() and the definition of the key of keyMap)
                if (!targetMap) {
                    bucket.set(target, (targetMap = new Map()));
                }

                let keyMap = targetMap.get(key); // keyMap is both the value of targetMap and
                if (!keyMap) {
                    targetMap.set(key, (keyMap = new Set()))
                }

                keyMap.add(activeEffect); //The side effect function is ultimately stored in the Set structure
                return target[key]
            },
            set(target, key, newVal) {
                target[key] = newVal;
                //Get the value of the map set of the key of the object Map in the bucket, that is, all side effect functions are executed in a loop
                let targetMap = bucket.get(target);
                if (!targetMap) return;
                let effects = targetMap.get(key);
                effects.forEach(fn => fn())
                return true
            }
        })

        function effect(fn) {
            //Assign the function to activeEffect. When the data is hijacked and activeEffect has a value, it will be set to the bucket where the side effects are stored. When the data changes are intercepted, the corresponding function is obtained and executed.
            activeEffect = fn;
            fn();
        }

        // The side effect function is executed once
        effect(() => {
            document.body.innerText = obj.text
        })
        effect(() => {
            document.title = obj.a
        })

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge. Vue entry skill treeVue2 responsive detecting changes 39487 people are learning the system