1. Introduction

I have always considered vUE’s data-responsive mechanism to be its soul, which is one of the reasons I prefer VUE. On October 5, 2019, UVU released the preview version of Vue3.0 source code, in which the responsive mechanism was also rewritten by the new syntax in ES6, I sort out the implementation principle of Vue3.0 data binding for your reference.

2.Vue2. X data binding mechanism

DefineProperty is used to intercept objects, add set and GET methods to the attributes of objects, collect dependencies in get method, and notify dependency update view in set method. However, this mechanism has certain drawbacks:

  • Deep recursive traversal of objects is a waste of memory
  • Object.definePropertyYou cannot listen for array changes, so you need to manually wrap array method hijacking
  • Object overget/setMethod to add a key-value pair directly cannot bind the newly added key-value pair

Here is a brief description of the data binding mechanism for VUe2 2.x:

Object to intercept

function observer(target){
    // If it is not an object data type, return it directly
    if(! isObject(target)){return target
    }
    // Redefine key
    for(let key in target){
        defineReactive(target,key,target[key])
    }
}

function isObject(target){
    return typeof target === "object"&& target ! = =null;
}

function defineReactive(obj,key,value){
    if(isObject(value)){
        observer(value); // Values for object types require deep recursive hijacking
    }
    
    Object.defineProperty(obj,key,{
        get(){
            Collect dependencies in the get method
            return value
        },
        set(newVal){
            if(newVal ! == value){// Need to continue hijacking for object type
                if(isObject(value)){
                    observer(value);
                }
                update(); // Trigger an update in the set method}}})}function update(){
    console.log('update view')}let obj = {name:'youxuan'}
observer(obj);
obj.name = 'webyouxuan';
Copy the code

Array method hijacking

const oldProtoMehtods = Array.prototype;
const proto = Object.create(oldProtoMehtods);
['push'.'pop'.'shift'.'unshift'. ] .forEach(method= >{
    Object.defineProperty(proto,method,{
        get(){
            update();
            oldProtoMehtods[method].call(this. arguments) } }) })function observer(target){
    // If it is not an object data type, return it directly
    if(typeoftarget ! = ='object') {return target
    }
    
    // Add a custom data hijacking method for an array
    if(Array.isArray(target)){
        Object.setPrototypeOf(target,proto);
        // Observr is applied to each item in the array
        for(let i = 0; i < target.length; i++){ observer(target[i]) }return
    };
    // Redefine key
    for(let key in target){
        defineReactive(target,key,target[key])
    }
}
Copy the code

The data binding principle of VUe2.x is not explained here, but we will focus on Vue3.0.

3.Vue3.0 source directory analysis

├── Packages │ ├── Compiler-Core # all Platforms │ ├── Compiler-DOM # For The Browser compiler │ ├─ ReActivity # Data Response System │ ├─ │ ├─ Running-core # Running-Core # Running-Core # ├─ Running-dom # Its capabilities include handling native DOM apis, DOM events, and DOM attributes. │ ├ ─ ─ the runtime - test # written specifically for testing the runtime │ ├ ─ ─ server - the renderer # for SSR │ ├ ─ ─ Shared # help method │ ├ ─ ─ the template - explorer │ └ ─ ─ vue Build vue Runtime + CompilerCopy the code

Compiler compiler-core exposes the compiler API and baseCompile method. Compiler – DOM is based on compiler-core and wraps the compiler for the browser.

Runtime Runtime – Core The virtual DOM renderer, Vue components, and various Vue apis runtime-test format DOM structures into objects, Runtime: createApp; render; createApp

Reactivity is a separate data responsive system with core methods Reactive, effect, REF, computed

Vue integrated Compiler + Runtime

4. Vue3.0 early experience

Vue3.0 directory structure is very clear. For students who want to experience Vue3.0, the official scaffolding has also been released to support vuE-next version vuE-cli-plugin-vue-next. The specific experience method is as follows:

# in an existing Vue CLI project
vue add vue-next
Copy the code

Initialize a project using vue-CLI, then type vue add vue-next on the command line in the project root directory.

Please note: vuE-CLI version must be updated to V4.3.1, and vue-Router and Vuex are not currently supported by VUe3.0. – 20200511.

Here is a simple demo of vue3.0 code:

< the template > < div id = "app" > < div > the mouse coordinates X - {{X}} < / div > < div > mouse Y - {{Y}} < / div > < / div > < / template > < script > / / similar to react Import {ref, onMounted, onUnmounted} from "vue"; Function usePosition() {const x = ref(0); const y = ref(0); function update(e) { x.value = e.pageX; y.value = e.pageY; } onMounted(() => { window.addEventListener("mousemove", update); }); onUnmounted(() => { window.removeEventListener("mousemove", update); }); return { x, y }; } export default { setup() { const { x, y } = usePosition(); Return {x, y}; }}; </script>Copy the code

5. Parse Vue3.0 data binding

Before learning Vue3.0, it is necessary to master Proxy, Reflect and Map and Set data structures in ES6. If you are not familiar with them, it is recommended to master these knowledge first.

Let’s start by looking at how data binding is implemented in Vue3.0

const person = Vue.reactive({name:'cangshudada'}); // The person object has become responsive data
Vue.effect(() = >{ The effect method fires once immediately
    console.log(person.name);
})

person.name = Hamster Big;; // The effect method is fired again when the property is modified

Copy the code

Source code is prepared by TS, because there may be unfamiliar with TS students, here we use JS to write from 0 to achieve the principle, then look at the source code will be more relaxed!

5.1 reactive implementation

/ * * * *@description Generate a responsive object *@param {any} target
 * @returns* /
function reactive(target) {
    // Create a responsive object
    return createReactiveObject(target);
}

/ * * * *@description Check whether it is object *@param {any} target
 * @returns {boolean}* /
function isObject(target) {
    return typeof target === "object"&& target ! = =null;
}

/ * * * *@description Create responsive objects *@param {any} target
 * @returns* /
function createReactiveObject(target){
    // Check whether target is an object
    if(! isObject(target)){return target;
    }
    
    // get set delete ... Object methods
    const handlers = {
        get(target,key,receiver){ / / value
            let res = Reflect.get(target,key,receiver);
            return res;
        },
        set(target,key,value,receiver){ // Change/add attributes
            let result = Reflect.set(target,key,value,receiver);
            return result;
        },
        deleteProperty(target,key){ // Delete attributes
            const result = Reflect.deleteProperty(target,key);
            returnresult; }}// Start the proxy
    observed = new Proxy(target,handlers);
    return observed;
}
let p = reactive({name:'cangshudada'});
console.log(p.name); / / value
p.name = Hamster Big; / / set
delete p.name; / / delete
Copy the code

But such objects may exist

const person ={
    name: 'cangshudada'.age: 24.pets: {
        dog: {
            name: 'guagua'.age: 1
        },
        cat: {
            name: 'gugu'.age: 2}}}Copy the code

So we need to continue to implement the proxy in the case of multi-layer object nesting:

get(target, key, receiver) {
    / / value
    const res = Reflect.get(target, key, receiver);
    return isObject(res) ? reactive(res) : res; Vue2.0 will recursively add getters and setters all the time
}
Copy the code

Let’s move on to the array case

Proxy can support array by default, so we don’t need like Vue2. X as an array encapsulation and in which to hijack my way to monitor data changes, but when we change the array will still be able to find problem, that is the change of the array will trigger two set, respectively is the length of the array change and the change of the index value, Next we need to mask the problem of multiple triggers.

set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = target.hasOwnProperty(key);
    const result = Reflect.set(target, key, value, receiver);
    // Check whether it is new or modified
    if(! hadKey) {// If there is no key, it is added
        trigger(target, 'add', key)
    } else if(oldValue ! == value) {// Prevent set from being triggered more than once when the array is repeatedly manipulating the index or length
        trigger(target, 'set', key)
    }
    return result;
}
Copy the code

At this point, the problem of array is also solved. Finally, the compatibility of repeated proxy for the same object is solved by using WeakMap. In this case, the complete reactive implementation is as follows:

const toProxy = new WeakMap(a);// Store the proxied object
const toRaw = new WeakMap(a);// Store the proxied object

/ * * * *@description Generate a responsive object *@param {any} target
 * @returns* /
function reactive(target) {
    // Create a responsive object
    return createReactiveObject(target);
}


/ * * * *@description Check whether it is object *@param {any} target
 * @returns {boolean}* /
function isObject(target) {
    return typeof target === "object"&& target ! = =null;
}


/ * * * *@description Determines whether the key * exists in the object@param {object} target
 * @param {object} key
 * @returns {boolean}* /
function hasOwn(target, key) {
    return target.hasOwnProperty(key);
}

/ * * * *@description Create responsive objects *@param {any} target
 * @returns* /
function createReactiveObject(target) {

    // Whether it is an object
    if(! isObject(target)) {return target;
    }

    // Determine the proxied object
    let observed = toProxy.get(target);

    if (observed) { // Determine whether the proxy is used
        return observed;
    }
    if (toRaw.has(target)) { // Determine the condition of duplicate proxy, if duplicate proxy
        return target;
    }

    const handlers = {
        get(target, key, receiver) {
            / / value
            const res = Reflect.get(target, key, receiver);
            track(target, 'get', key);// Collect dependencies
            return isObject(res) ? reactive(res) : res; Vue2.0 will recursively add getters and setters all the time
        },
        set(target, key, value, receiver) {
            const oldValue = target[key];
            const hadKey = hasOwn(target, key);
            const result = Reflect.set(target, key, value, receiver);
            // Check whether it is new or modified
            if(! hadKey) {// If there is no key, it is added
                trigger(target, 'add', key) // Trigger dependency update - increment
            } else if(oldValue ! == value) {// Prevent set from being triggered more than once when the array is repeatedly manipulating the index or length
                trigger(target, 'set', key) // Trigger dependency update - modify
            }
            return result;
        },
        deleteProperty(target, key) {
            trigger(target, 'delete', key);// Trigger dependency update - delete
            const result = Reflect.deleteProperty(target, key);
            returnresult; }};// Start the proxy
    observed = new Proxy(target, handlers);
    toProxy.set(target, observed);
    toRaw.set(observed, target); // create a mapping table
    return observed;
}

// Object status
const person = reactive({ name: 'cangshudada' });
console.log('person.name >>', person.name); / / to get
person.name = Hamster Big; / / set
delete person.name; / / delete
person.age = 12;// Can delegate to keys added directly to the object
person.age = 24

// Can directly proxy arrays and duplicate proxies
const ary = reactive([1.2.3.4]);
ary.push(5)
const ary1 = reactive(ary); // The repeated proxy returns the previously propped object
Copy the code

By now, the reactive method has been basically implemented, and the next step is to collect dependencies and trigger dependency updates like the logic in Vue2. X, where track is used to collect dependencies and mainly collects effects, and trigger is used to notify effect updates

5.2 effect to realize

Effect, which stands for side effect, is invoked first by default, and then triggered again if the data changes.

const person = Vue.reactive({name:'cangshudada'}); // The person object has become responsive data
Vue.effect(() = >{ The effect method fires once immediately
    console.log(person.name);
})

person.name = Hamster Big;; // The effect method is fired again when the property is modified
Copy the code

Let’s implement the effect function first


/ * * * *@description Effect function *@param {function} Fn callback function *@returns* /
function effect(fn) {
    const effect = createReactiveEffect(fn); // Create a reactive effect
    effect(); // Execute first
    return effect;
}

// place the response effect
const activeReactiveEffectStack = []; 

/ * * * * *@param {function} Fn callback function *@returns* /
function createReactiveEffect(fn) {
    const effect = function () {
        // effect
        return run(effect, fn);
    };
    return effect;
}


/ * * * *@param {function} Effect Responsive effect *@param {function} Fn callback function *@returns* /
function run(effect, fn) {
    try {
        activeReactiveEffectStack.push(effect);
        return fn(); // Effect can be stored to the corresponding key property when fn executes
    } finally{ activeReactiveEffectStack.pop(effect); }}Copy the code

The get method may fire when fn() is called, which fires the track function called in get above

const targetMap = new WeakMap(a);function track(target,type,key){
    // Check for effect
    const effect = activeReactiveEffectStack[activeReactiveEffectStack.length-1];
    if(effect){
        const depsMap = targetMap.get(target);
        if(! depsMap){// Add a Map object if there are no dependent array objects
            targetMap.set(target,depsMap = new Map());
        }
        const deps = depsMap.get(target); 
        if(! deps){// Add Set array if deps does not exist
            depsMap.set(key,(deps = new Set()));
        }
        if(! deps.has(effect)){// Add effect to the dependent array if it is not present in depsdeps.add(effect); }}}Copy the code

Trigger execution is triggered when the property is updated, and effects are found in the corresponding storage set according to the key value

function trigger(target,type,key){
    const depsMap = targetMap.get(target);
    if(! depsMap){return
    }
    const deps = depsMap.get(key);
    if(deps){
        deps.forEach(effect= >{ effect(); }}})Copy the code

At this time, there is still the problem of length. For example, we listen the length of array in effect. At this time, because we set the mechanism that length change does not trigger trigger function in the set function above, we need to add judgment in trigger to accommodate this situation

function trigger(target, type, key) {
  const depsMap = targetMap.get(target);
  if(! depsMap) {return;
  }
  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach(effect= > {
      deps();
    });
  }
  // If the current update type is increment, effect using array length should also be executed
  if (type === "add") {
    const lengthDeps = depsMap.get("length");
    if (lengthDeps) {
      lengthDeps.forEach(effect= >{ effect(); }); }}}Copy the code

5.3 ref implementation

Ref can also convert the raw data type into responsive data, which requires the.value attribute to get the value

/* * * @description Specifies the different types of data to be processed by reactive
function convert(target) {
  return isObject(target) ? reactive(target) : target;
}

function ref(raw) {
  raw = convert(raw);
  const v = {
    _isRef:true.// Identifies the ref type
    get value() {
      track(v, "get"."");
      return raw;
    },
    set value(newVal) {
      raw = newVal;
      trigger(v,'set'.' '); }};return v;
}
Copy the code

In this case, if the following situation occurs, each call will have an extra.value, which is very troublesome, so we have to make compatibility for this situation

const name = ref('cangshudada');
const person = reactive({
    c_Name: name
});
console.log(person.c_Name.value); // every time you call c.a, you have to add.value
Copy the code

This requires compatibility in the GET function

get(target, key, receiver) {
    / / value
    const res = Reflect.get(target, key, receiver);
    // the value of a ref cannot be returned directly
    if(res._isRef){
        return res.value
    }
    track(target, 'get', key);// Collect dependencies
    return isObject(res) ? reactive(res) : res; / / lazy agent
}
Copy the code

5.4 the computed to realize

In previous versions of computed function, the function was triggered only when the value of the monitored variable changed, which is very useful in real projects. Now vue3.0 has rewritten the responsive data mechanism, which also leads to the rewriting of computed. Let’s see how computed is implemented in Vue3.0. First let’s look at usage

const person = reactive({name:'cangshudada'});
const _computed = computed(() = >{
  console.log('Computed executes')  
  return `${person.name} --- xixi`;
})
// If _computed. Value is not computed, the callback function is not executed, unless the listener changes n times and only once
console.log(_computed.value);// Computed implements cangshudada -- xixi
console.log(_computed.value);// cangshudada --- xixi
person.name = Hamster Big;
console.log(_computed.value);// Computed performs hamster greatly - xixi
Copy the code

The computed to realize

function computed(fn){
  let dirty = true; // The value is triggered for the first time
  const runner = effect(fn,{ // Indicates that this effect is lazy
    lazy:true./ / lazy to perform
    scheduler:() = >{ // This method is called when the attribute of the dependency changes, instead of re-executing effect, the dirty is not updated if the dependency is not updated, thus not triggering runner(), the caching mechanism
      dirty = true; }});let value;
  return {
    _isRef:true.get value() {if(dirty){
        value = runner(); // The runner continues to collect dependencies
        dirty = false;
      } 
      return value; // No computed callback is performed if value changes}}}Copy the code

Modifying the effect function You are advised to view it in 5.2 Effect

function effect(fn,options) {
  let effect = createReactiveEffect(fn,options);
  if(! options.lazy){// If it is lazy, it is not executed immediately
    effect();
  }
  return effect;
}

function createReactiveEffect(fn,options) {
  const effect = function() {
    return run(effect, fn);
  };
  effect.scheduler = options.scheduler;

  return effect;
}
Copy the code

On the trigger

deps.forEach(effect= > {
  if(effect.scheduler){ // Effect does not need to be executed if there is a scheduler
    effect.scheduler(); // Set dirty to true so that the runner method can be re-executed the next time the value changes
  }else{
    effect(); // Otherwise, execute effect normally}});Copy the code
const person = reactive({name:'cangshudada'});
const _computed = computed(() = >{
  console.log('Computed executes')  
  return `${person.name} --- xixi`;
})
// If _computed. Value is not computed, the callback function is not executed, unless the listener changes n times and only once
console.log(_computed.value);
person.name = Hamster Big; // Changing the value does not trigger recalculation, but does change dirty to true
console.log(_computed.value); // The get function is triggered and runner() is called to re-call the calculation method
Copy the code

6. Summary

At this point we will Vue3.0 source code reactivity part of the parsing is complete! Understand the vUE data binding mechanism for after the interview or later application has a great help, of course, this article is just a brief analysis of this part, clear data binding this part of the logic and thought to read the source code of this part I believe you will have more harvest. This article was actually published earlier and was not released to the community at that time. Today, I sorted it out and re-published it. If you are interested, I will also analyze and interpret other modules. Of course, if you have different understanding or opinion of this article, welcome to comment on the exchange, learning and progress together ~