preface

In the last chapter, we have made a rough analysis of the entire Vue source code (still in the draft box, which needs to be sorted out before being released), but there are still many things that have not been analyzed in depth. I will carry out further analysis through the following important points.

  1. In-depth understanding of Vue responsive principles (data interception)
  2. Dig deeper into how vue.js does “dependency collection” to accurately track all changes
  3. Learn more about the Virtual DOM
  4. Learn more about vue.js batch asynchronous update strategy
  5. In-depth understanding of vue.js internal operation mechanism, understand the principle behind calling each API

In this chapter, we analyze 1. In-depth understanding of Vue responsiveness principle (data interception).

initState

As we analyzed in the previous chapter, when initializing a Vue instance, the _init method is executed, which executes the initState method. This method is very important because when we instantiate our new Vue object, Pass the parameters props, methods,data, computed, and watch processing. The code is as follows:

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      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

In this section, we will only examine the handling of data, the initData(VM) method, which looks like this (exception handling is removed) :

  function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
 
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."), vm ); }}if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if(! isReserved(key)) { proxy(vm,"_data", key); }}// observe data
    observe(data, true /* asRootData */);
  }
Copy the code

From the above code analysis, the first thing that can be inferred is the following

Conclusion:

  1. The key in data must not have the same name as the key in methods and props
  2. proxy(vm, "_data", key);Just todataThe properties inside are remounted (proxy) invmFor example, we can access it in two waysdataThe data inside, such asvm.visibilityorvm._data.visibilityThe effect is the same.observe(data, true /* asRootData */);Is the most important method, and let’s analyze this method

observe

Observe Chinese translation is observed, is the original data into an observable object, its code is as follows (deleted some logical judgment) :

  function observe (value, asRootData) {
    ob = new Observer(value);
  }
Copy the code

This method creates a new Observer object with the following constructor:

  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__'.this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value); }};Copy the code

Todos is an Array. We’ll look at Array handling later. We call this.walk, which iterates through each property of the Object:

  Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive?1(obj, keys[i]); }};Copy the code

defineReactive? Method 1 rewraps data with object.defineProperty, adding a getter and setter for each property to intercept data

  function defineReactive? 1 (obj, key, val, customSetter, shallow) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if((! getter || setter) &&arguments.length === 2) {
      val = obj[key];
    }

    varchildOb = ! shallow && observe(val);Object.defineProperty(obj, key, {
      enumerable: true.configurable: true.get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) { dependArray(value); }}}return value
      },
      set: function reactiveSetter (newVal) {
        var value = getter ? getter.call(obj) : val;
        /* eslint-disable no-self-compare */
        if(newVal === value || (newVal ! == newVal && value ! == value)) {return
        }
        /* eslint-enable no-self-compare */
        if (customSetter) {
          customSetter();
        }
        // #7981: for accessor properties without setter
        if(getter && ! setter) {return }
        if (setter) {
          setter.call(obj, newVal);
        } else {
          val = newVal;
        }
        childOb = !shallow && observe(newVal);
        dep.notify();
      }
    });
  }
Copy the code

defineReactive? DefineProperty is used to set properties that already exist in data. Getters, setters, get, and set are set to work.

var childOb = ! shallow && observe(val); Observe is a recursive callback that intercepts all child attributes.

The todos property in data is an array, and we return to the observe method, whose main purpose is to pass ob = new Observer(value); To generate an Observer object:

  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__'.this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value); }};Copy the code

It can be seen here that there is a special treatment for Array. Let’s analyze protoAugment method in detail below

ProtoAugment (array)

protoAugment(value, arrayMethods); The first parameter is our Array, and the second parameter arrayMethods needs to be analyzed, which is the special processing of Array in Vue.

The source file is under vue\ SRC \core\observer\array.js,

  1. First of all, based onArray.prototypeThe prototype creates a new objectarrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
Copy the code
  1. Rewrite theArrayThe following7Methods:
var methodsToPatch = [
    'push'.'pop'.'shift'.'unshift'.'splice'.'sort'.'reverse'
  ];
Copy the code
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, functionmutator (... args) { const result = original.apply(this, args) const ob = this.__ob__let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
Copy the code

Conclusion: Vue only listens for the seven methods mentioned above, and other Array methods will not trigger Vue’s bidirectional binding. For example, using concat,map, etc., does not trigger bidirectional binding.

this.$set

The above has analyzed the data listening of Object and Array, but the above situation is that when initializing the Vue instance, we already know what attributes there are in data, and then intercept the data of each attribute. Now there is a situation that if we need to dynamically add attributes to data, what should we do?

$set(this. NewTodo,”name”, ’30’); $set(this. NewTodo,”name”, ’30’);

  function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key intarget && ! (keyin Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      warn(
        'Avoid adding reactive properties to a Vue instance or its root $data ' +
        'at runtime - declare it upfront in the data option.'
      );
      return val
    }
    if(! ob) { target[key] = val;return val
    }
    defineReactive?1(ob.value, key, val);
    ob.dep.notify();
    return val
  }
Copy the code

From the above analysis, the following points should be noted when using the $set method:

  1. The target is notundefined.null.string.number.symbol.booleanSix basic data types
  2. Target cannot be mounted directly inVueInstance object, and cannot be mounted directly at rootdataOn the properties

$set finally calls defineReactive? 1(ob.value, key, val); Method to dynamically add a property and add getters and setters to that property

For dynamically added properties that also need to update the view dynamically, call ob.dep.notify(); Method to dynamically update the view

conclusion

  1. ifdataA property is aObject, will convert it, mainly to do the following two things:
  1. Add one to the object__ob__Property, which is aObserverobject
  1. traversedataSay there is an attribute (‘key’), passObject.definePropertySet itsgettersetterTo intercept data
  1. ifdata(or child property) is oneArrayIs converted to its prototypearrayMethods(based onArray.prototypeThe prototype creates a new object, but redefines seven methods ‘push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’) to perform the pairingArray(This is why only these seven methods can implement bidirectional binding for Vue arrays)

We’ve looked at the Vue responsive principle in this article, but we’ll continue to look more closely at how vue.js does “dependency collection” to accurately track required changes