One line analysis of Vue source code two way binding principle

I. Preface

Today, let's step by step realize the two-way binding of Vue. First, introduce a picture, this picture source , he wrote this article very well. You can go and have a look. The significance of my article lies in the analysis of source code and some understanding of myself. Please point out the shortcomings. Thank you. Maybe it's not easy to understand just by looking at the diagram. Then analyze the diagram from the perspective of source code. Looking back at this diagram, you'll understand a lot. You can't live much. Let's start.

2, Source code analysis

Take me first One line analysis of Vue source code (I) overall analysis , if you haven't read my first article, you can see that data monitoring binding is performed in initState method.

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); } // First, check whether there is props initialization in options
    //First, check whether there are methods related to initialization methods in options, because we are talking about two-way binding now. We will not talk about it here, but will add later
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm); // Initialization data
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); } // Initialize computed
    if (opts.watch && opts.watch !== nativeWatch) { // Initialize listening
      initWatch(vm, opts.watch);
    }
  }
  function initData (vm) {
    var data = vm.$options.data; // Get data from vm.$options
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};// If data is method execution GetData, the internal part of GetData method is to execute data.call to get the return value of method
    if (!isPlainObject(data)) {
    //To determine whether data is a pure object or not, the following warning requires that the data function return an object
      data = {};
      warn(
        'data functions should return an object:\n' +
        'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
        vm
      );
    }
    // proxy data on instance
    var keys = Object.keys(data); // Get the key of the data object
    var props = vm.$options.props; // Get props in options
    var methods = vm.$options.methods;// Get methods in options
    var i = keys.length;
    while (i--) { // Cyclic traversal
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) { 
        // Use Object.prototype.hasOwnProperty to determine whether the key defined in data is also defined in methods
        // That's why we define a variable in data and a variable with the same name in methods,
        // The following props are the same, so data and methods props cannot have variables with the same name, because no matter the method
        // Either props or data will be placed on the instance of Vue. If the name is the same, I don't know if you want to take data 
        // methods or porps
          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)) { //Intercept the agent if the data key does not start with $or \
        proxy(vm, "_data", key); // because
      }
    }
    // observe data
    observe(data, true /* asRootData */);
  }

Because the value in data is placed in_data, you can only get the value of MSG through this._data.msg. Isn't this what I need to get every value? It's hard for programmers to accept this. What we want to get from this.xxx is what I want, how simple it is, so there's a proxy method. Use Object.defineProperty

 var sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
  };

  function proxy (target, sourceKey, key) { 
  // target represents vm, sourceKey represents_data, and key represents the value you want to take
  // This means that if you want to use this.msg to get the MSG, in fact, you need to point to this.u data.msg to get the same effect,
  // Therefore, this method is called agent, which is simple and convenient
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

Next, the last observe method

  function observe (value, asRootData) {// Pass in data, and whether the second parameter is the root node data
    if (!isObject(value) || value instanceof VNode) { 
    // If the incoming data is not an object or the VNdode virtual node is not input, the judgment is returned for the end of recursion judgment, which will be discussed later
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
     // A series of judgments start from the Observer in the top figure
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

  var Observer = function Observer (value) {
    this.value = value; // Put data on the value of Observer this
    this.dep = new Dep(); // Top picture of Dep message management center
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) { // If the data passed in is an array
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else { // Passed in is not an array
      this.walk(value);
    }
  };
  var Dep = function Dep () {
    this.id = uid++; 
    this.subs = []; // Define a list of subscribers
  };

  Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);  // Get the key of data and start to listen circularly
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };
  function defineReactive$$1 (
    obj, // data
    key, // A key in data
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key); // Get the modifier of key in data
    if (property && property.configurable === false) { //If the key cannot be written back
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) { // If there are only two parameters passed in, it means data listening
      val = obj[key];
    }

    var childOb = !shallow && observe(val); // Recursion. The above observe first determines the correspondence. If val is still an object
    //Then recursion, if it is not an object, the end recursion
    // Below is Vue's two-way binding core using Object.defineProperty data hijacking
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
      // When the value is taken, for example, to get MSG, use this.msg to enter this method
        var value = getter ? getter.call(obj) : val; //If the property itself has a getter to use itself, otherwise get the original value
        if (Dep.target) {
        //It's more complicated here. It's about Watcher
        // If there is a new watcher, the Watcher will tell the message manager to add subscribers
          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)) {
        // If the new value is equal to the old value, return directly without changing
          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); // observe hijack the set value
        dep.notify(); // Message manager to notify
      }
    });
  }

The following three codes are to add subscribers when taking values

Dep.prototype.depend = function depend () {
    if (Dep.target) { // Here, Dep.target is the Watcher. When there is a new Watcher, it will be put into Dep.target
      Dep.target.addDep(this);
    }
  };
  Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id; // The id of Dep is used to determine whether there are already the same subscribers among subscribers. If not, add or not
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub); // Add to Dep's sub array list
  };

The following code snippet is to notify Watcher of an update when the value is set

  Dep.prototype.notify = function notify () {
    // stabilize the subscriber list first
    var subs = this.subs.slice();
    if (!config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();  
    }
  };
  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };
  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };

I'm sure you are a little dizzy now. Maybe what I said is not very clear. Because there are many ways to encapsulate and always jump, it's very complicated. If you don't see from the whole, there are too many ways to jump, it's a little difficult. Now I'm going to draw a flow chart to smooth the thinking.


In fact, the two-way binding principle is mainly divided into four modules: Observer, DEP, Watcher, and Compile. First, the Observer uses Object.defineProperty to hijack data. When using the publish and subscribe design mode, the Observer is monitored. When taking a value, the message management center is informed to add a listener When setting the value, the Watcher notifies you to change, and the Compile resolves and compiles again, and updates the corresponding view. If you want to convert the vue template, such as the v-model, {}} into html, you need to Compile and parse it through Compile, so this is the basic principle of two-way binding. There is not too much talk about Compile here, because it is still learning. The Compile is converted into the AST virtual tree through the template, and it is changing There are many difficulties in the process of converting VNode to html. Please update later

Published 48 original articles, won praise 8, visited 40000+
Private letter follow

Keywords: Vue

Added by venradio on Wed, 15 Jan 2020 05:32:44 +0200