vue3.0 Source Parsing 1: Data Binding Principles (Up)

Posted by reinmedia on Sat, 27 Jun 2020 19:18:18 +0200

Preamble Introduction

Beginning with this article, we are officially entering the vue3.0 source code parsing process.Personally, starting with ceateApp is not the best way to learn, so let's start with composition-api responsive principles and work together to learn what dramatic changes vue3.0 brings.

The serialized articles are roughly the same and may change as they change:
1 Data binding principles (top)
2 Data binding principles (2)
3 computed and watch Principles
4 Event System
5 ceateApp
6 Initialize mounted and patch processes.
7 diff algorithm differs from 2.0
8 Compile compiler series
...

A proxy-based Observer

1 What is proxy

Proxy objects are used to define custom behavior for basic operations such as attribute lookup, assignment, enumeration, function calls, and so on.

proxy is a new feature of es6. To intercept certain behaviors of target object target (such as attribute lookup, assignment, enumeration, function call, etc.) through interception methods in handler object.

/* target: Target object, the target object to be wrapped with a Proxy (can be any type of object, including a native array, function, or even another proxy). */
/* handler: An object that typically takes functions as attributes, each of which defines the behavior of the proxy when performing various operations. */ 
const proxy = new Proxy(target, handler);

2 Why use proxy instead of proxy?

3.0 will bring with it a Proxy-based observer implementation that provides a full range of responsiveness over the language (JavaScript - Annotations), eliminating the current Vue 2 series based on Object.defineProperty There are some limitations, such as: 1. monitoring the actions of adding and deleting attributes; 2. modifying the array based on subscript; and 2. length Modified monitoring; 3 support for Map, Set, WeakMap, and WeakSet.

vue2.0 for Object.defineProperty As an implementation of the responsive principle, there are limitations, such as the inability to listen for subscript-based modifications of arrays, the lack of support for Map, Set, WeakMap, and WeakSet, so proxy was used to solve these problems, which also means vue3.0 will discard compatibility with lower browsers (compatible versions above ie11).

Basic usage of hander object in 3 proxy

Capturers used responsively by vue3.0 (highlighted next)

Handler.has() The catcher for the -> in operator.(vue3.0 used)
Handler.getCapturer for () -> property read operations.(vue3.0 used)
Handler.set() -> Property Settings * Capturer for operation.(vue3.0 used)
Handler.deleteProperty() -> the catcher for the delete operator.(vue3.0 used)
Handler.ownKeys() ->Object.getOwnPropertyNamesMethods and Object.getOwnPropertySymbols The capturer for the method.(vue3.0 used)

vue3.0 Responsive Unused Capturer (Interested Students can study it)

Handler.getPrototypeOf() ->Object.getPrototypeOfThe capturer for the method.
Handler.setPrototypeOf() ->Object.setPrototypeOfThe capturer for the method.
Handler.isExtensible() ->Object.isExtensibleThe capturer for the method.
Handler.preventExtensions() ->Object.preventExtensionsThe capturer for the method.
Handler.getOwnPropertyDescriptor() ->Object.getOwnPropertyDescriptorThe capturer for the method.
Handler.defineProperty() ->Object.definePropertyThe capturer for the method.
Handler.apply() -> Capturer for function call operations.
Handler.construct() -> the catcher for the new operator.

(1) has capturer

has(target, propKey)

target:Target Object

propKey:Property name to intercept

Role: Intercepts actions that determine whether a target object contains an attribute propKey

Intercept operation: propKey in proxy; does not contain for...in loop

Corresponding Reflect:Reflect.has(target, propKey)

_Example:

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

(2) get catcher

get(target, propKey, receiver)

target:Target Object

propKey:Property name to intercept

receiver: proxy instance

Return: Returns the read property

Role: Intercept reading of object properties

Intercept operation: proxy[propKey] or point 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 */

Exceptional 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 error is reported
  }
})
console.log( proxPerson.age ) /* Errors will occur */

(3) set capturer

set(target,propKey, value,receiver)

target:Target Object

propKey:Property name to intercept

Value:The newly set property value

receiver: proxy instance

Return: The return true operation in strict mode succeeded; otherwise it failed with an error

Role: Intercept the attribute assignment of an object

Intercept 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 normal age range */
        throw new RangeError('The age seems invalid')
      }
    }
    obj[prop] = value
    // Indicate 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         // Throw an exception: Uncaught RangeError: The age appears invalid

When the object's property writable is false, it 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} indicates that the modification failed

(4) deleteProperty capturer

deleteProperty(target, propKey)

target:Target Object

propKey:Property name to intercept

Return: Only true is returned in strict mode, otherwise error is reported

Role: Intercepts deletion of the propKey property of a target object

Intercept 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 deleting fruits :',target[prop])
    return delete target[prop]
  }
});
delete proxy.apple
console.log(foot)

/*
Run result:
'Currently deleting fruit: apple'
{  banana:'Banana'}
*/

Special case: Attributes cannot be deleted when they are not configurable attributes

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 Capturer

ownKeys(target)

Target: target object

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

Role: Intercept operations to get key values

Intercept operations:

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

/* and Object.keys() The filtering property is the same, only traversable attributes of the target itself are returned */
for(let prop in p) { 
 console.log('prop-',prop) /* prop-a */
}

/* Return only non-Symbol attributes returned by the interceptor, whether they are attributes on the target or not */
const ownNames = Object.getOwnPropertyNames(p)  /* ['a', 'c', 'b'] */

/* Returns only the attributes of the Symbol returned by the interceptor, whether they are attributes on the target or not*/
const ownSymbols = Object.getOwnPropertySymbols(p)// [Symbol(foo), Symbol(bar)]

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

How 2 vue3.0 establishes a response

There are two ways for vue3.0 to establish a response:
The first is to use reactives in composition-api to construct a response directly. The emergence of composition-api allows us to use setup() functions directly in the.Vue file to handle most of the previous logic, that is, we do not need to declare life cycle, data(){} function, watch{} in export default {}.Computed {}, etc., instead, we use the vue3.0 reactive watch lifecycle API in the setup function to achieve the same effect, which improves code reuse like react-hooks and is more logical.

The second is in the traditional form of data () {return {}}. Instead of giving up support for vue2.0 writing, vue3.0 is fully compatible with vue2.0 writing, providing applyOptions to handle Vue components in the form of options.However, the data, watch, computed and other processing logic in options uses the API corresponding processing in composition-api.

1 composition-api reactive

reactive is equivalent to the currentVue.observable() API, reactively processed functions can become responsive data, similar to the vue in option api handling the return value of data functions.

Let's try a todoList demo.


const { reactive , onMounted } = Vue
setup(){
    const state = reactive({
        count:0,
        todoList:[]
    })
    /* Lifecycle mounted */
    onMounted(() => {
       console.log('mounted')
    })
    /* Increase the number of count s */
    function add(){
        state.count++
    } 
    /* Reduce the number of count s */
    function del(){
        state.count--
    }
    /* Add to-do */
    function addTodo(id,title,content){
        state.todoList.push({
            id,
            title,
            content,
            done:false
        })
    }
    /* Complete agency work */
    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
                }
            }
        }
    }
}

A Preliminary Study on the Principle of Three Responses

Different types of Reactive

vue3.0 can introduce different API methods based on business needs.Need here

① reactive

Create a responsive reactive that returns a proxy object, which is a deep-level recursion, that is, if an expanded attribute value is found to be of reference type and referenced, it will also be processed with a reactive recursion.And attributes can be modified.

② shallowReactive

Create a responsive shallowReactive that returns a proxy object.The difference with reactive is that only one level of response is established, that is, if an expanded property is found to be a reference type, it is not recursive.

③ readonly

The returned proxy handles objects that can be expanded for recursive processing, but the properties are read-only and cannot be modified.Pros can be passed to subcomponents for use.

④ shallowReadonly

Returns the processed proxy object, but building a responsive property is read-only, does not expand references, and does not recursively convert, which can be used to create props proxy objects for stateful components.

Stored 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 there are multiple reactions in a component, there will be many original objects for the proxy object and its proxy.In order to establish relationships between proxy objects and original objects, WeakMap is used in vue3.0 to store these object relationships.WeakMaps maintains a weak reference to the object referenced by the key name, which is not taken into account by the garbage collection mechanism.As long as other references to the referenced object are cleared, the garbage collection mechanism releases the memory occupied by the object.That is, once no longer needed, the key name object and the corresponding key-value pair in WeakMap 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 to meet the one-way data flow scenario where the previous props cannot be modified.
Let's focus on the next four weakMap storage relationships.

rawToReactive

Key value pair: {[targetObject]: obseved}

Target: The target object value (understood here as the first parameter of the reactive).)
obsered: a proxy object that has been proxyed.

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

rawToReadonly

Key value pair: {[target]: obseved}

Target: the target object.
obsered: a proxy object for a read-only property that has been proxyed.

readonlyToRaw
Storage status is the opposite of rawToReadonly.

reactive entry resolution

Next we'll focus on reactive.

Reactive ({... Object}) entry

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

The role of the reactive function is to produce a proxy using the createReactiveObject method and to give different processing methods for different data types.

createReactiveObject

As mentioned earlier, let's look at what happens to the 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 a new Proxy */
  let observed = toProxy.get(target) /* { [target] : obseved  } */
  if (observed !== void 0) { /* If the target object has already been responsively processed, the observed object of the proxy is returned directly */
    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 either collectionHandlers or baseHandlers */
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
   /* TODO: Create Responsive Objects  */
  observed = new Proxy(target, handlers)
  /* target Associate 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 source above is as follows:
First, determine if the target object has been proxy-responsive, and if so, return the object directly.
(2) Then choose whether the target object is a [Set, Map, WeakMap, WeakSet] data type to use collectionHandlers or baseHandlers->mutableHandlers passed in by reactive s as hander objects of proxy.
Finally, create an observed by really using the new proxy, then save the target and observed key-value pairs by rawToReactive reactiveToRaw.

Rough flowchart:

What did baseHandlers do?

As to what hander did, we will continue to discuss in the next chapter due to the relationship between length and author's time.

Reference documents:

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

Topics: Javascript Attribute Vue React