Vue3.0 source code analysis one: data binding principle (on)

Posted Jun 27, 202011 min read

Introduction

u=1777986988,1364946555&fm=11&gp=0.jpg

Starting from this article, we formally entered the vue3.0 source code analysis process. I personally think that starting with ceateApp is not the best learning solution, so we first start with the composition-api responsive principle and jointly learn what earth-shaking changes brought by vue3.0.

The serialized article is roughly like this and may change at any time according to changes:
1 Principles of Data Binding(Part 1)
2 Principles of Data Binding(Part 2)
3 The principle of computed and watch
4 Event system
5 ceateApp
6 Initialize the mounted and patch process.
7 Difference between diff algorithm and 2.0
8 Compile the compiler series
...

A proxy-based Observer

1 What is proxy

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

Proxy is a new feature of es6. In order to affect the target, it mainly intercepts certain behaviors of the target object(such as attribute search, assignment, enumeration, function call, etc.) through the interception method in the handler object.

/* target:target object, the target object to be wrapped with Proxy(can be any type of object, including native arrays, functions, or even another proxy). */
/* handler:An object that usually takes functions as attributes. The functions in each attribute define the behavior of the proxy when performing various operations. */
const proxy = new Proxy(target, handler);

2 Why use proxy, the pros and cons of using proxy

3.0 will bring a Proxy-based observer implementation, which can provide a full range of responsive capabilities covering the language(JavaScript-Annotation), eliminating some of the limitations of the current Vue 2 series based on Object.defineProperty, these limitations include :1 monitoring of the addition and deletion of attributes; 2 monitoring of array-based subscript modification and .length modification; 3 support for Map, Set, WeakMap and WeakSet;

vue2.0 uses Object.defineProperty as the implementation of the responsive principle, but it has its limitations, such as cannot listen to the modification of the array based on the subscript, does not support the defects of Map, Set, WeakMap and WeakSet , So proxy is used to solve these problems, which also means that vue3.0 will give up compatibility with lower version browsers(compatible version ie11 and above).

3 Basic usage of handler object in proxy

vue3.0 responsive capturer(more on that later)

handler.has() -> in operator catcher. (used in vue3.0)
handler.get() -> attribute read catcher for operation. (used in vue3.0)
handler.set() -> Attribute setting* The catcher of the operation. (used in vue3.0)
handler.deleteProperty() -> delete operator catcher. (used in vue3.0)
handler.ownKeys() -> Catcher for Object.getOwnPropertyNames method and Object.getOwnPropertySymbols method (used in vue3.0)

vue3.0 responsive capturer not used(interested students can study it)

handler.getPrototypeOf() -> Object.getPrototypeOf method catcher.
handler.setPrototypeOf() -> A catcher for the Object.setPrototypeOf method.
handler.isExtensible() -> Object.isExtensible method catcher.
handler.preventExtensions() -> Object.preventExtensions method of catcher.
handler.getOwnPropertyDescriptor() -> Object.getOwnPropertyDescriptor method catcher.
handler.defineProperty() -> Object.defineProperty method catcher.
handler.apply() -> Function of function call operation.
handler.construct() -> new operator catcher.

has catcher

has(target, propKey)

target:target object

propKey:property name to be intercepted

Role:Intercept to determine whether the target object contains the property propKey

Interception operation:propKey in proxy; does not include for...in loop

Corresponding Reflect:Reflect.has(target, propKey)

? Example:

const handler = {
    has(target, propKey){
        /*
        * Do your operation
        */
        return propKey in target
    }
}
const proxy = new Proxy(target, handler)

get catcher

get(target, propKey, receiver)

target:target object

propKey:property name to be intercepted

receiver:proxy instance

Returns:Returns the attributes read

Role:intercept the reading of object properties

Interception operation:proxy[propKey]or dot operator

Corresponding Reflect:Reflect.get(target, propertyKey[, receiver])

? Example:

const handler = {

    get:function(obj, prop) {
        return prop in obj? obj[prop]:'No such fruit';
    }
}

const foot = new Proxy({}, handler)
foot.apple ='Apple'
foot.banana ='Banana';

console.log(foot.apple, foot.banana); /* Apple Banana */
console.log('pig' in foot, foot.pig); /* false no such fruit */

Special case

const person = {};
Object.defineProperty(person,'age', {
  value:18,
  writable:false,
  configurable:false
})
const proxPerson = new Proxy(person, {
  get(target,propKey) {
    return 20
    //Should return 18; cannot return other values, otherwise an error is reported
  }
})
console.log(proxPerson.age) /* will report an error */

set catcher

set(target,propKey, value,receiver)

target:target object

propKey:property name to be intercepted

value:the newly set attribute value

receiver:proxy instance

Returns:returns true in strict mode. The operation succeeds; otherwise, it fails and an error is reported.

Role:intercept the attribute assignment operation of the object

Interception operation:proxy[propkey]= value

Corresponding Reflect:Reflect.set(obj, prop, value, receiver)

let validator = {
  set:function(obj, prop, value) {
    if(prop ==='age') {
      if(!Number.isInteger(value)) {/* if age is not an integer */
        throw new TypeError('The age is not an integer')
      }
      if(value> 200) {/* beyond the normal age range */
        throw new RangeError('The age seems invalid')
      }
    }
    obj[prop]= value
    //indicates success
    return true
  }
}
let person = new Proxy({}, validator)
person.age = 100
console.log(person.age) //100
person.age ='young' //Throw an exception:Uncaught TypeError:The age is not an integer
person.age = 300 //Exception thrown:Uncaught RangeError:The age seems invalid

When the property writable of the object is false, the property cannot be modified in the interceptor

const person = {};
Object.defineProperty(person,'age', {
    value:18,
    writable:false,
    configurable:true,
});

const handler = {
    set:function(obj, prop, value, receiver) {
        return Reflect.set(...arguments);
    },
};
const proxy = new Proxy(person, handler);
proxy.age = 20;
console.log(person) //{age:18} means the modification failed

deleteProperty catcher

deleteProperty(target, propKey)

target:target object

propKey:property name to be intercepted

Return:Only return true in strict mode, otherwise error

Role:intercept the operation of deleting the propKey attribute of the target object

Interception operation:delete proxy[propKey]

Corresponding Reflect:Reflect.delete(obj, prop)

var foot = {apple:'Apple', banana:'Banana}
var proxy = new Proxy(foot, {
  deleteProperty(target, prop) {
    console.log('Currently delete fruit:',target[prop])
    return delete target[prop]
  }
});
delete proxy.apple
console.log(foot)

/*
operation result:
'Currently deleted fruit:apple'
{banana:'Banana'}
*/

Special case:When the attribute is not configurable, it cannot be deleted

var foot = {apple:'Apple'}
Object.defineProperty(foot,'banana', {
   value:'Banana',
   configurable:false
})
var proxy = new Proxy(foot, {
  deleteProperty(target, prop) {
    return delete target[prop];
  }
})
delete proxy.banana /* no effect */
console.log(foot)

ownKeys catcher

ownKeys(target)

target:target object

Returns:array(array elements must be characters or symbols, other types of errors)

Function:Intercept the operation of obtaining key value

Interception operation:

1 Object.getOwnPropertyNames(proxy)

2 Object.getOwnPropertySymbols(proxy)

3 Object.keys(proxy)

4 for...in...loop

Corresponding Reflect:Reflect.ownKeys()

var obj = {a:10, [Symbol.for('foo')]:2 };
Object.defineProperty(obj,'c', {
   value:3,
   enumerable:false
})
var p = new Proxy(obj, {
 ownKeys(target) {
   return [...Reflect.ownKeys(target),'b', Symbol.for('bar')]
 }
})
const keys = Object.keys(p) //['a']
//Automatically filter out Symbol/non-self/non-traversable attributes

/* Same filtering properties as Object.keys(), only return traversable attributes of target itself */
for(let prop in p) {
 console.log('prop-',prop) /* prop-a */
}

/* Only return the non-Symbol attributes returned by the interceptor, regardless of whether they are attributes on the target */
const ownNames = Object.getOwnPropertyNames(p) /* ['a','c','b']*/

/* Only return the attributes of the Symbol returned by the interceptor, regardless of the attributes on the target */
const ownSymbols = Object.getOwnPropertySymbols(p)//[Symbol(foo), Symbol(bar)]

/*Return all values   returned by the interceptor*/
const ownKeys = Reflect.ownKeys(p)
//['a','c',Symbol(foo),'b',Symbol(bar)]

Two vue3.0 how to build responsive

There are two ways to build responsive vue3.0:
The first is to use reactive in composition-api to directly build responsive. With the emergence of composition-api, we can directly use the setup() function in the .vue file to handle most of the previous logic, that is to say, we don t need to In export default{ }, the life cycle is declared, data(){} function, watch{}, computed{}, etc. Instead, we use the vue3.0 reactive watch life cycle api in the setup function to achieve the same effect. This is like react-hooks to improve the code reuse rate, and the logic is stronger.

The second is to use the traditional data(){ return{}} form, vue3.0 did not give up support for vue2.0 writing, but it is fully compatible with vue2.0 writing, providing applyOptions To handle vue components in the form of options. However, the processing logic such as data, watch, and computed in the options still use the API corresponding processing in the composition-api.

1 composition-api reactive

Reactive is equivalent to the current Vue.observable() API. After the reactive processing, the function can become responsive data, similar to the return value of the vue processing data function in the option api.

We use a todoList demo to try it out.

const {reactive, onMounted} = Vue
setup(){
    const state = reactive({
        count:0,
        todoList:[]
    })
    /* life cycle mounted */
    onMounted(() => {
       console.log('mounted')
    })
    /* Increase count */
    function add(){
        state.count++
    }
    /* Reduce the count */
    function del(){
        state.count--
    }
    /* Add to-do items */
    function addTodo(id,title,content){
        state.todoList.push({
            id,
            title,
            content,
            done:false
        })
    }
    /* Complete the charge d'affaires */
    function complete(id){
        for(let i = 0; i< state.todoList.length; i++){
            const currentTodo = state.todoList[i]
            if(id === currentTodo.id){
                state.todoList[i]= {
                    ...currentTodo,
                    done:true
                }
                break
            }
        }
    }
    return {
        state,
        add,
        del,
        addTodo,
        complete
    }
}

2 options data

There is no difference between options and vue2.0

export default {
    data(){
        return{
            count:0,
            todoList:[]
        }
    },
    mounted(){
        console.log('mounted')
    }
    methods:{
        add(){
            this.count++
        },
        del(){
            this.count--
        },
        addTodo(id,title,content){
           this.todoList.push({
               id,
               title,
               content,
               done:false
           })
        },
        complete(id){
            for(let i = 0; i< this.todoList.length; i++){
                const currentTodo = this.todoList[i]
                if(id === currentTodo.id){
                    this.todoList[i]= {
                        ...currentTodo,
                        done:true
                    }
                    break
                }
            }
        }
    }
}

Three preliminary study of responsive principle

Different types of Reactive

Vue3.0 can introduce different API methods according to business needs. Need here

reactive

Create a reactive reactive and return a proxy object. This reactive can be deeply recursive, that is, if the expanded attribute value is found to be reference type and is referenced, it will also be processed with reactiverecursively . And attributes can be modified.

shallowReactive

Establish responsive shallowReactive and return the proxy object. The difference from reactive is that only one layer of responsiveness is established, which means that if the expansion attribute is found to be reference type, it will not recursively.

readonly

The objects processed by the returned proxy can be recursively processed, but the attributes are read-only and cannot be modified. Props can be passed to subcomponents.

shallowReadonly

The processed proxy object is returned, but the establishment of responsive properties is read-only, and the reference is not expanded nor recursively converted. This can be used to create props proxy objects for stateful components.

Save objects and proxy

We mentioned above. The object processed and returned by Reactive is a proxy object. Assuming that there are many components, or that it is reactive multiple times in a component, there will be a lot of proxy objects and the original object that it proxies. In order to establish the relationship between the proxy object and the original object, vue3.0 uses WeakMap to store these object relationships. WeakMaps maintains a weak reference to the object referenced by the key name, that is, the garbage collection mechanism does not take the reference into account. As long as all other references to the referenced object are cleared, the garbage collection mechanism will release the memory occupied by the object. That is to say, once it is no longer needed, the key name object in WeakMap and the corresponding key-value pair will disappear automatically, without manually deleting the reference.

const rawToReactive = new WeakMap<any, any>()
const reactiveToRaw = new WeakMap<any, any>()
const rawToReadonly = new WeakMap<any, any>() /* read only */
const readonlyToRaw = new WeakMap<any, any>() /* read only */

Vue3.0 uses readonly to set whether the object intercepted by the interceptor can be modified, which can meet the previous unidirectional data flow scenario where the props cannot be modified.
We next focus on the storage relationship of the next four weakMap.

rawToReactive

Key-value pair:{[targetObject]:obseved}

target(key):target object value(here can be understood as the first parameter of reactive.)
obsered(value):the proxy object after proxying.

reactiveToRaw
reactiveToRaw stores exactly the opposite of rawToReactive key-value pairs.
Key-value pair {[obseved]:targetObject}

rawToReadonly

Key-value pair:{[target]:obseved}

target(key):the target object.
obsered(value):a proxy object with a read-only attribute after proxying.

readonlyToRaw
The storage state is just the opposite of rawToReadonly.

reactive entry analysis

Next, we focus on starting with reactive.

reactive({ ...object }) entrance

/* TODO:*/
export function reactive(target:object) {
  if(readonlyToRaw.has(target)) {
    return target
  }
  return createReactiveObject(
    target, /* target object */
    rawToReactive, /* {[targetObject]:obseved} */
    reactiveToRaw, /* {[obseved]:targetObject} */
    mutableHandlers, /* handle basic data types and reference data types */
    mutableCollectionHandlers /* Used to handle Set, Map, WeakMap, WeakSet types */
 )
}

The purpose of the reactive function is to generate a proxy through the createReactiveObject method, and different processing methods are given for different data types.

createReactiveObject

The createReactiveObject mentioned earlier, let's take a look at what happened to createReactiveObject.

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
function createReactiveObject(
  target:unknown,
  toProxy:WeakMap<any, any>,
  toRaw:WeakMap<any, any>,
  baseHandlers:ProxyHandler<any>,
  collectionHandlers:ProxyHandler<any>

) {
/* Determine whether the target object is affected /
/
observed is a function proxied by new Proxy /
let observed = toProxy.get(target) /
{[target]:obseved} /
if(observed !== void 0) {/
If the target object has been processed responsively, then directly return the proxy observed object /
return observed
}
if(toRaw.has(target)) {/
{[observed]:target} /
return target
}
/
If the target object is of type Set, Map, WeakMap, WeakSet, then the hander function is collectionHandlers or the target function is baseHandlers /
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
:baseHandlers
/
TODO:create responsive objects /
observed = new Proxy(target, handlers)
/
target is associated with observed /
toProxy.set(target, observed)
toRaw.set(observed, target)
/
return observed object */
return observed
}

The general process of creating a proxy object from the above source code is this:
First determine whether the target object has been proxy-responsive, and if so, return the object directly.
Then determine whether the target object is [Set, Map, WeakMap, WeakSet ]data type to choose whether to use collectionHandlers, or **baseHandlers-> is the mutableHandlers passed in by reactive as the proxy handler object .
Finally, create an observed by actually using new proxy, and then save the target and observed key-value pairs through rawToReactive reactiveToRaw.

General flow chart:

ABF8F111-C0D1-4012-8E8A-07BB69A87ACD.jpg

What did baseHandlers do?

As for what exactly the handler does, due to the relationship between the space and the author's time, we will continue to discuss in the next chapter.

Reference documents:

Detailed Proxy https://www.cnblogs.com/lyraL...