My previous article vUE responsive principle learning (a) describes some simple knowledge of VUE data responsive principle. As we all know, Vue data attribute, is the default depth listening, this time we again in-depth analysis, Observer source implementation.

Let’s warm up with a deep copy

Since the data property is deeply listened on, let’s first implement a simple deep copy ourselves to understand the idea.

The principle of deep copy is a bit like recursion. In fact, when you encounter a reference type, you call your own function to parse it again.

function deepCopy(source) {
    // Type check, if not reference type or all equal to null, return directly
    if (source === null || typeofsource ! = ='object') {
        return source;
    }

    let isArray = Array.isArray(source),
        result = isArray ? [] : {};
        
    // Iterate over attributes
    if (isArray) {
        for(let i = 0, len = source.length; i < len; i++) {
            let val = source[i];
            // typeof [] === 'object', typeof {} === 'object'
            // Consider typeof null === 'object'
            if (val && typeof val === 'object') {
                result[i] = deepCopy(val);
            } else{ result[i] = val; }}/ / short
        // result = source.map(item => {
        // return (item && typeof item === 'object') ? deepCopy(item) : item
        // });
    } else {
        const keys = Object.keys(source);
        for(let i = 0, len = keys.length; i < len; i++) {
            let key = keys[i],
                val = source[key];
            if (val && typeof val === 'object') {
                result[key] = deepCopy(val);
            } else{ result[key] = val; }}/ / short
        // keys.forEach((key) => {
        // let val = source[key];
        // result[key] = (val && typeof val === 'object') ? deepCopy(val) : val;
        // });
    }
    
    return result;
}
Copy the code

Why simple deep copy, because regexps, dates, prototype chains, DOM/BOM objects are not taken into account. It’s not easy to write a deep copy.

Some students may ask, why not just a for in solution. As follows:

function deepCopy(source) {
    let result = Array.isArray(source) ? [] : {};
    
    // Iterate over the object
    for(let key in source) {
        let val = source[key];
        result[key] = (val && typeof val === 'object')? deepCopy(val) : val; }return result;
}
Copy the code

One of the pain points of for in is that non-built-in methods on the prototype chain are also iterated. Examples include the developer’s own methods that extend the object’s prototype.

Some students might say, add hasOwnProperty to solve it. If it is Object, it can be resolved, but if it is Array, it cannot get the index of the Array.

One more thing to note about for in is that for in can continue, but forEach methods on arrays can’t. Because the internal implementation of forEach executes the functions you pass in sequence in a for loop.

Analyze Vue’s Observer

I’m just adding comments to the code here, and I suggest you open the source code.

Source code: the Vue project under SRC/core/observer/index, js

Vue encapsulates the Observer as a class

Observer
export class Observer {
    value: any;
    dep: Dep;
    vmCount: number; // number of vms that has this object as root $data

    constructor(value: any) {
        this.value = value
        this.dep = new Dep()
        this.vmCount = 0
        // Each time an object is observed, an __ob__ attribute is added to the object with the value of the current Observer instance
        // Of course, the premise is that the value itself is an array or object, and not the underlying data type, such as a number, string, etc.
        def(value, '__ob__'.this)   
        
        // If it is an array
        if (Array.isArray(value)) {
            // These two lines of code will be explained later
            // This code is used to enable the array operator function
            // That is, when we use the PUSH Pop Splice array API, we can also trigger the data response to update the view.
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)
            
            // Walk through the array and observe
            this.observeArray(value)
        } else {
            // Walk through the object and observe
            // There is a case where value is not Object,
            Keys takes a number and returns an empty array if it is a string.
            this.walk(value)
        }
    }

    // Walk through the object and observe
    walk(obj: Object) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            DefineReactive calls the observe method internally,
            // Observe internally calls the Observer constructor
            defineReactive(obj, keys[i])
        }
    }

    // Walk through the array and observe
    observeArray(items: Array<any>) {
        for (let i = 0, l = items.length; i < l; i++) {
            Observe internally calls the Observer constructor
            observe(items[i])
        }
    }
}

function protoAugment(target, src: Object, keys: any) {
    target.__proto__ = src
}

function copyAugment(target: Object, src: Object, keys: Array<string>) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i]
        def(target, key, src[key])
    }
}
Copy the code

Observe, def, defineReactive, and so on

observefunction

Used to call the Observer constructor

export function observe(value: any, asRootData: ? boolean) :Observer | void {
    // If it is not an object or a VNode instance, return it directly.
    if(! isObject(value) || valueinstanceof VNode) {
        return
    }
    // Define a variable to store an Observer instance
    let ob: Observer | void
    // If the object has already been observed, Vue automatically assigns an __ob__ attribute to the object to avoid repeated observations
    If the object already has an __ob__ attribute, indicating that it has been observed, then __ob__ is returned
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__
    } else if (
        shouldObserve &&       // Whether to observe! isServerRendering() &&// Non-server rendering
        (Array.isArray(value) || isPlainObject(value)) &&     // Is an array or Object Object
        Object.isExtensible(value) &&     // Whether the object is extensible, that is, whether new attributes can be added to the object! value._isVue// Non-vUE instance
    ) {
        ob = new Observer(value) 
    }
    if (asRootData && ob) {  // It's not clear yet, but we can ignore it for now
        ob.vmCount++
    }  
    return ob  // Return an Observer instance
}
Copy the code

Observe that the observe function simply returns an Observer instance with a little extra judgment. To make it easier to understand, we can simply shorten the code:

// This is much clearer
function observe(value) {
    let ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.___ob___
    } else {
        ob = new Observer(value) 
    }
    return ob;
}
Copy the code
deffunction

This is a wrapper around Object.defineProperty

export function def(obj: Object, key: string, val: any, enumerable? : boolean) {
    Object.defineProperty(obj, key, {
        value: val,
        // The default is not enumerable, which means that the __ob__ attribute Vue adds to the object is not iterated normallyenumerable: !! enumerable,writable: true.configurable: true})}Copy the code
defineReactivefunction

The defineReactive function does more than collect dependencies at initialization and trigger dependencies when attributes are changed

export function defineReactive(
    obj: Object,     //Observed object Key: string,//Object property val: any,//Does the user assign a value to the property, customSetter? :? Function,//Additional user-defined set shallow? : boolean//Depth observation) {
    // Used to collect dependencies
    const dep = new Dep()

    // If it cannot be modified, return directly
    const property = Object.getOwnPropertyDescriptor(obj, key)
    if (property && property.configurable === false) {
        return
    }
    
    
    // If the user has not defined get or set on the object, and the user has not passed val
    // Evaluates the initial value of the object and assigns it to the val parameter
    const getter = property && property.get
    const setter = property && property.set
    if((! getter || setter) &&arguments.length === 2) {
        val = obj[key]
    }

    / /! Shallow indicates deep observation. If shallow is not true, it indicates the default deep observation
    // To observe in depth, run the observe object method
    letchildOb = ! shallow && observe(val)Object.defineProperty(obj, key, {
        enumerable: true.configurable: true.get: function reactiveGetter() {
            // Get the original value of the object
            const value = getter ? getter.call(obj) : val
            
            // Collect dependencies. Collecting and triggering dependencies is a larger process that will be discussed later
            if (Dep.target) {
                dep.depend()
                if (childOb) {
                    childOb.dep.depend()
                    if (Array.isArray(value)) {
                        dependArray(value)
                    }
                }
            }
            
            // Return the original value of the object
            return value
        },
        set: function reactiveSetter(newVal) {
            // Get the original value of the object
            const value = getter ? getter.call(obj) : val

            // Check whether the value changes
            // (newVal ! == newVal && value ! == value) to determine NaN! == NaN
            if(newVal === value || (newVal ! == newVal && value ! == value)) {return
            }
            
            // for non-production environments, additional custom setters are triggered
            if(process.env.NODE_ENV ! = ='production' && customSetter) {
                customSetter()
            }
            
            // Fire the original setter for the object, if not, override the old value with the new value (val)
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }

            // If it is a deep observation, observe again after the property is changedchildOb = ! shallow && observe(newVal)// Trigger dependencies. Collecting and triggering dependencies is a larger process that will be discussed later
            dep.notify()
        }
    })
}
Copy the code
Where is entrance

So, where is the initialization entry of the Vue observation object, of course, at the initialization of the Vue instance, i.e., at new Vue?

Source code: the Vue project under SRC/core/instance/index, js

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)    // This method is defined in initMixin
}

// This is where the initMixin function extends a _init method on Vue's Prototype
// The this._init(options) method is executed when we are new Vue
initMixin(Vue)  

stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
Copy the code

The initMixin function extends a _init method on vue. prototype that has an initState function for data initialization

initState(vm)   // vm is the current Vue instance, and Vue assigns the data attribute we passed to vm._data
Copy the code

The initState function internally executes a piece of code that looks at the data property on the VM instance

Source code: the Vue project under SRC/core/instance/state. Js. I’ve commented out the useless code, leaving only the code that initializes data.

export function initState(vm: Component) {
    // vm._watchers = []
    // const opts = vm.$options
    // if (opts.props) initProps(vm, opts.props)
    // if (opts.methods) initMethods(vm, opts.methods)
    
    // If the data attribute is passed in
    // Data is the data attribute we passed in when new Vue
    if (opts.data) {    
        // initData internally normalizes the data property we pass in.
        // If data is not a function, observe(data)
        // If data is a function, perform the function first, assign the returned value to data, override the original value, and observe(data).
        // This is why data can be passed as a function when we write components
        initData(vm)    
    } else {
        // If no data attribute is passed in, observe an empty object
        observe(vm._data = {}, true /* asRootData */)}// if (opts.computed) initComputed(vm, opts.computed)
    // if (opts.watch && opts.watch ! == nativeWatch) {
    // initWatch(vm, opts.watch)
    // }
}
Copy the code
conclusion

What does Vue do to the data property we passed in when we new it?

  1. If we pass indataIs a function that executes the function first to get the return value. And assign overridesdata. If an object is passed in, no action is done.
  2. performobserve(data)
    • Observe does this internallynew Observer(data)
    • new Observer(data)Will be indataExtend one on the objectAn enumerationThe properties of the__ob__, this property is very useful.
    • ifdataIs an array
      • performobserveArray(data). This method iteratesdataObject and executes on each array itemobserve.Refer to Step 2 for the rest of the process
    • ifdataIs an object
      • performwalk(data). This method iteratesdataObject and execute on each propertydefineReactive.
      • defineReactiveInternally, it executes on the object properties passed inobserveRefer to Step 2 for the rest of the process

Space and energy are limited, about the function of protoAugment and copyAugment, how to collect dependency and trigger dependency implementation in defineReactive will be discussed later.

Please also point out any mistakes in the content of the article.

Reference:

How does JavaScript fully implement deep Clone objects

Vue technology insider