When it comes to the principle of VUE, the most important thing is responsiveness, virtual DOM and DIff algorithm, template compilation. Today, we will go into the responsiveness of VUE, discuss the implementation principle and deficiency of VUe2. X responsiveness, and how to rewrite the implementation scheme of responsiveness in VUe3.0 version.

1. What is reactive

Vue is an MVVM framework. The core of MVVM is data-driven view. Generally speaking, users do not directly manipulate DOM, but manipulate data. Similarly, user actions (events) on the view change the data in turn. Reactive, on the other hand, is the first step in implementing data-driven views, that is, listening for changes in data, so that when users set data, they can notify vUE internally for view updates, for example

<template> <div> <div> {{name}} </div> < button@click ="changeName"> </button> </div> </template> <script> export default { data () { return { name: 'A' } }, methods: { changeName () { this.name = 'B' } } } </script>Copy the code

In the code above, after clicking the button, the name property will change, and the page displayed A will change to B

2. vue2.xImplementing responsiveness

2.1 Core API — Object.defineProperty()

I think most of you know vUE at some point or another. The core of vUE responsiveness is Object.defineProperty(). Here’s a quick review

const data = {}
let name = 'A'
Object.defineProperty(data, 'name', {
    get () {
        return name
    },
    set (val) {
        name = val
    }
})
console.log(data.name) // get()
data.name = 'B' // set()
Copy the code

As we can see in the above code, object.defineProperty () is used to define a property (method) for an Object and provide two internal implementations of set and get to get or set the property (method).

2.2 How to achieve responsiveness

First, we define an initial data as follows

const data = {
    name: 'A'.age: 18.isStudent: true.gender: 'male'.girlFriend: {
        name: 'B'.age: 'the'.isStudent: true.gender: 'female'.parents: {
            mother: {
                name: 'C'.age: '44'.isStudent: false.gender: 'female'
            },
            father: {
                name: 'D'.age: '46'.isStudent: false.gender: 'male'}}, hobbies: ['basketball'.'one-piece'.'football'.'hiking']}Copy the code

We also define a method to render the view

function renderView () {
    // Render the view when the data changes
}
Copy the code

And a core method that implements responsiveness. This method takes three parameters: target is the data object itself, key and value are the key of the object and its corresponding value

function bindReactive (target, key, value) {}Copy the code

Finally, we define entry methods that implement responsiveness

function reactive() {/ /... }Copy the code

Our final call is theta

const reactiveData = reactive(data)
Copy the code

2.2.1 For primitive types and objects

The above data, we simulated a simple information introduction of a person, you can see the object of the character values of strings, numbers, booleans, objects, arrays. For primitive types like strings, numbers, booleans, we just return them

function reactive (target) {
    // First, the object is not returned directly
    if (typeoftarget ! = ='object' || target === null) {
        return target
    }
}
const reactiveData = reactive(data)
Copy the code

If the field value is a reference type like an Object, we need to traverse the Object and set object.defineProperty () to each key value of the Object separately. Note that this procedure needs to be called recursively, because objects can be nested in multiple layers as shown in the data we present. We define a function, bindReactive, to describe the process of listening on a reactive object

function bindReactive (target, key, value) {
    Object.defineProperty(target, key, {
        get () {
            return value
        },
        set (val) {
            value = val
            // Triggers a view update
            renderView()
        }
    })
}

function reactive (target) {
    // First, the object is not returned directly
    if (typeoftarget ! = ='object' || target === null) {
        return target
    }
    // Iterate over the object, listening for each key responsively
    for (let key in target) {
        bindReactive(target, key, target[key])
    }
}
const reactiveData = reactive(data)
Copy the code

Considering recursion, we need to recursively call Reactive to listen for object properties when executing the core method bindReactive, and also recursively call reactive to update data when setting (updating) data, so our core method bindReactive changes to

function bindReactive (target, key, value) {
    reactive(value)
    Object.defineProperty(target, key, {
        get () {
            return value
        },
        set (val) {
            reactive(val)
            value = val
            // Triggers a view update
            renderView()
        }
    })
}
function reactive (target) {
    // First, the object is not returned directly
    if (typeoftarget ! = ='object' || target === null) {
        return target
    }
    // Iterate over the object, listening for each key responsively
    for (let key in target) {
        bindReactive(target, key, target[key])
    }
}
const reactiveData = reactive(data)
Copy the code

The above code can be optimized so that when a set is set, if the new value is the same as the previous value, it does not trigger a view update, so our method becomes

function bindReactive (target, key, value) {
    reactive(value)
    Object.defineProperty(target, key, {
        get () {
            return value
        },
        set (newVal) {
            if(newVal ! == value) { reactive(newVal) value = newVal// Triggers a view update
                renderView()
            }
        }
    })
}
function reactive (target) {
    // First, the object is not returned directly
    if (typeoftarget ! = ='object' || target === null) {
        return target
    }
    // Iterate over the object, listening for each key responsively
    for (let key in target) {
        bindReactive(target, key, target[key])
    }
}
const reactiveData = reactive(data)
Copy the code

So far, we’ve implemented responsive listening for primitive types and objects, and when the data changes, we call the renderView method (which can do anything) to update the view after the data has been updated.

2.2.2 For arrays

Clearly, although Object.defineProperty() does a good job of responding to primitive types and normal objects, this method does nothing for arrays. So how does VUE implement responsive listening on arrays? Let’s first go back to the official vue documentation

As you can see, vUE can respond to array changes when executing push, POP, shift, unshift, etc., triggering view updates.

But as we all know, the array native methods don’t have the ability to update the view in response, so we know that Vue must have overwritten the array methods, so now the question is how do arrays implement responsiveness instead of how do arrays rewrite the array API.

Create (prototype); this method creates an Object whose prototype points to the prototype parameter. Then we can rewrite these array methods as well:

// Array prototype
const prototype = Array.prototype
// create a newPrototype object whose prototype is an array prototype (hence all array apis on newPrototype)
const newPrototype = Object.create(prototype)
const methods = ['push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse']
methods.forEach(method= > {
    newPrototype[method] = (a)= > {
        prototype[method].call(this. args)// View update
        renderView()
    }
})
Copy the code

To implement array responsiveness, we’re refining the entry method reactive

function bindReactive (target, key, value) {
    reactive(value)
    Object.defineProperty(target, key, {
        get () {
            return value
        },
        set (newVal) {
            if(newVal ! == value) { reactive(newVal) value = newVal// Triggers a view update
                renderView()
            }
        }
    })
}
function reactive (target) {
    // First, the object is not returned directly
    if (typeoftarget ! = ='object' || target === null) {
        return target
    }
     // For arrays, prototype modification
    if (Array.isArray(target)) {
        target.__proto__ = newPrototype
    }
    // Iterate over the object, listening for each key responsively
    for (let key in target) {
        bindReactive(target, key, target[key])
    }
}
const reactiveData = reactive(data)
Copy the code

So far, we have explained the responsivity principle of the VUe2. X version

2.3 Disadvantages of vue2. X version responsive implementation scheme

Through our analysis, we can also see the disadvantages of vue2. X version reactive implementation:

  1. Object.defineProperty()The API does not natively listen on arrays responsively
  2. For deeply nested data, recursion costs a lot of performance
  3. We noticed that,Object.defineProperty()The problem with this implementation, and with arrays, is that there is no way to listen for subsequent manual additions and delets of attribute elements, such as arrays. Setting and changing values directly through indexes does not trigger view updates, as VUE doesvue.setandvue.deletesuchapiBut it is inconvenient after all

3. vue3.0Implementing responsiveness

Not long ago, Vue3.0 was officially released. Although there is no official promotion yet, some changes in it are worth our attention and learning

3.1 ProxyandReflect

Because of the problems with the reactive implementation in vue2.x, vue officially rewrote the reactive implementation completely in vue 3.0, using Proxy and Reflect instead of Object.defineProperty().

3.1.1 Proxy

First, let’s look at the definition of Proxy by MDN:

The Proxy object is used to define custom behavior for fundamental operations(e.g. property lookup, assignment, enumeration, function invocation, etc).
Copy the code

Proxy objects are used to define custom behavior for basic operations (such as lookup, assignment, enumeration, function call, etc.).

let proxy = new Proxy(target, handler)
Copy the code

Note that target can be a native array.

  1. target:ProxyThe wrapped target object (which can be any type of object, includingThe original array, function, or even another proxy).
  2. handler: an object whose properties are functions that define the behavior of the agent when an operation is performed.

Here’s an example:

let handler = {
    get: function(target, name){
        return name in target ? target[name] : 'sorry, not found'; }};let p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b);    // 1, undefined
console.log('c' in p, p.c);    // false, 'sorry, not found'
Copy the code

3.1.2 Reflect

Let’s start with MDN’s definition of Reflect:

Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible.
Copy the code

Reflect is a built-in object that provides methods for intercepting JavaScript operations. These methods are the same as proxy handlers. Reflect is not a function object, so it is not constructible.

The Refelct object provides many methods. Here are just a few of the common methods used to implement the reactive style:

  1. Reflect.get(): Gets the value of an attribute on an object, similar totarget[name].
  2. Reflect.set(): A function that assigns values to attributes. Returns aBoolean, if the update succeededtrue.
  3. Reflect.has(): Determines whether an object has an attribute, andinOperators do exactly the same thing.
  4. Reflect.deleteProperty(): is the delete operator of the function, equivalent to the delete target[name].

Therefore, we can combine Proxy and Reflect to perform responsive listening

3.2 ProxyandReflectImplementing responsiveness

Here is the code directly posted to modify the method we implemented earlier:

function bindReactive (target) {
    if (typeoftarget ! = ='object' || target == null) {
        // If it is not an object or array, return it directly
        return target
    }
    // Since Proxy natively supports arrays, there is no need to implement it yourself
    // if (Array.isArray(target)) {
    // target.__proto__ = newPrototype
    // }
    // Pass to Proxy handler
    const handler = {
        get(target, key) {
            const reflect = Reflect.get(target, key)
            // When we get an object attribute, Proxy only recurses to the acquired level, not to the sublevels
            return bindReactive(reflect)
        },
        set(target, key, val) {
            // Duplicate data is not processed
            if (val === target[key]) {
                return true
            }
            // We can do different things with the existing key
            if (Reflect.has(key)) {
               
            } else{}const success = Reflect.set(target, key, val)
            // Whether the setting succeeds
            return success 
        },
        deleteProperty(target, key) {
            const success = Reflect.deleteProperty(target, key)
             // Whether the deletion succeeds
            return success
        }
    }
    // Generate a proxy object
    const proxy = new Proxy(target, handler)
    return proxy
}
// Implement data responsive listening
const reactiveData = bindReactive(data)
Copy the code

In the above code, we can see that the problems existing in the vue2. X response have been solved well:

  1. ProxySupports listening on native arrays
  2. ProxyWill only recurse to the level needed to fetch data, will not continue to recurse
  3. ProxyYou can listen for manual addition and deletion of data

Is vue3.0’s responsive solution perfect? The answer is no, mainly because Proxy and Reflect have browser compatibility issues and cannot be polyfilled.

4. To summarize

In this paper, a detailed and in-depth analysis of the vUE responsive principle, for 2.x and 3.0 version of the implementation of the difference, each has advantages and disadvantages, no scheme is perfect, I believe that in the future, when the browser compatibility problem is less and less, life will be better!