Vue bidirectional binding process

This is the overall operation flow chart of VUE. This section mainly records the process of bidirectional binding, dependency collection and other parts of the content. This article is only a personal learning record, if there is any wrong place, please feel free to point out.

  1. Obtain the contents of the VUE instance, including EL, data, props, etc. In this case, fetch the DATA of the VM and use the observe method to traverse. Pass this to the VM.
  <div id="app">
    <input type="text" v-model="text">
    {{ text }}
  </div>
Copy the code
    function Vue (options) {
      this.data = options.data;
      var data = this.data;
      observe(data, this);
      var id = options.el;
      var dom = nodeToFragment(document.getElementById(id), this);
      After compiling, return the DOM to the app
      document.getElementById(id).appendChild(dom); 
    }

    var vm = new Vue({
      el: 'app'.data: {
        text: 'hello world'}})Copy the code
  1. In the Observe method, iterate over all properties in data, entering and leaving each property responsively, passing in parameters (VM, property name, property value)
    function observe (obj, vm) {
      Object.keys(obj).forEach(function (key) { defineReactive(vm, key, obj[key]); })}Copy the code
  1. What to do in Define Active, first generate a DEP publisher. DefineProperty () : void Dep. Target () : void Dep. Target () : void Dep. Because there’s no new value assignment.
    function defineReactive (obj, key, val) {
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function () {
          // Add subscriber watcher to Dep
          if (Dep.target) dep.addSub(Dep.target);
          return val
        },
        set: function (newVal) {
          if (newVal === val) return
          val = newVal;
          // Send notifications as a publisherdep.notify(); }}); }Copy the code
  1. The subs array in the Dep is used to hold the observer. It has two static methods, one is to add subscribers, and the other is to notify all subscribers to update.
    function Dep () {
      this.subs = []
    }

    Dep.prototype = {
      addSub: function(sub) {
        this.subs.push(sub);
      },

      notify: function() {
        this.subs.forEach(function(sub) { sub.update(); }); }}Copy the code
  1. Then go down, get the ID, and declare a variable to receive the document fragment inserts.
    function Vue (options) {
      this.data = options.data;
      var data = this.data;
      observe(data, this);
      var id = options.el;
      var dom = nodeToFragment(document.getElementById(id), this);
      After compiling, return the DOM to the app
      document.getElementById(id).appendChild(dom); 
    }
Copy the code
  1. A DocumentFragment can be thought of as a node container. It can contain multiple child nodes. When we insert it into the DOM, only its child nodes will insert the target node, so think of it as a container for a group of nodes. Using DocumentFragment to process nodes is much faster and performs better than manipulating DOM directly. When Vue is compiled, it hijacks all the children of the mounted target (really hijacks; nodes in the DOM are automatically removed via appendChild) into the DocumentFragment. After some processing, The DocumentFragment is then returned as a whole and inserted into the mount target. This node is the DOM bound to el with an ID that we passed in earlier, and all nodes below it are processed and compiled using the Complie template.

Node (Generally, a node has at least three basic attributes: nodeType, nodeName, and nodeValue. The element node nodeType is 1, the attribute node nodeType is 2, and the text node nodeType is 3 (the text node contains text, space, and newline).

    function nodeToFragment (node, vm) {
      var flag = document.createDocumentFragment();
      var child;
      // It is necessary to explain this paragraph
      // First, all expressions must return a value, and assignment expressions are no exception
      While (child = node.firstChild) while (node.firstchild
      // Second, the appendChild method has a subtle aspect in that the child is removed from the original DOM after the call
      // so the second time through the loop, node.firstchild is no longer the firstChild
      while (child = node.firstChild) {
        compile(child, vm);
        flag.appendChild(child); // Hijack child nodes into document fragments
      }

      return flag
    }
Copy the code
  1. Based on the HTML structure written above, the incoming node will be executed three times.

We can see that the first time is the text node between div and input, the second time is the input element node, and the third time is the text node between the input and div.Node.nodetype === 3, then create a subscriber instance, pass it to the VM, the current node, The matched characters in {{}} are ‘text’. If it is an element node, such as input in our case, it will bind an event to the node, so that when we interact with the node, the event will be triggered, and the new value of the event will be updated to the VM’s data, thus implementing view-> Model. Views affect data. Node. value = vm[name]; node.value = vm[name]; Data affects the view.

    function compile (node, vm) {
      var reg = / \ {\ {(. *) \} \} /;
      // The node type is element
      if (node.nodeType === 1) {
        var attr = node.attributes;
        // Parse attributes
        for (var i = 0; i < attr.length; i++) {
          if (attr[i].nodeName == 'v-model') {
            var name = attr[i].nodeValue; // Get the attribute name of the V-Model binding
            node.addEventListener('input'.function (e) {
              // Assign a value to the corresponding data attribute to trigger the set method for that attribute
              vm[name] = e.target.value;
            });
            node.value = vm[name]; // Assign the value of data to the node
            node.removeAttribute('v-model'); }};new Watcher(vm, node, name, 'input');
      }
      // The node type is text
      if (node.nodeType === 3) {
        if (reg.test(node.nodeValue)) {
          var name = RegExp. $1;// Get the matched string
          name = name.trim();

          new Watcher(vm, node, name, 'text'); }}}Copy the code
  1. What did the subscribers do? Dep.target is actually a global object of Dep, which assigns a value of this, which is an instance of Warcher, and some other properties. Warcher has two static methods, one to update values and one to get values. The updata() method is triggered during the above object instantiation, so we go to this method and execute this.get(), which obtains the value of date in the VM instance and assigns it to the Warcher instance object. Attention!!!!! Because all attributes in VM data have been traversed and made responsive objects, this. Vm [this.name] calls trigger the get method inside.
    function Watcher (vm, node, name, nodeType) {
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.nodeType = nodeType;
      this.update();
      Dep.target = null;
    }
    Watcher.prototype = {
      update: function () {
        this.get();
        if (this.nodeType == 'text') {
          this.node.nodeValue = this.value;
        }
        if (this.nodeType == 'input') {
          this.node.value = this.value; }},// Get the attribute value in data
      get: function () {
        this.value = this.vm[this.name]; // Trigger get for the corresponding attribute}}Copy the code
  1. Enter the get method here, watch out!! The dep. target now has a value, which was assigned in the previous step, and is a Watcher instance object, which is then added to the observer array below the Dep publisher.
    function defineReactive (obj, key, val) {
      var dep = new Dep();
      Object.defineProperty(obj, key, {
        get: function () {
          // Add subscriber watcher to Dep
          if (Dep.target) dep.addSub(Dep.target);
          return val
        },
        set: function (newVal) {
          console.log('set the trigger');
          if (newVal === val) return
          val = newVal;
          // Send notifications as a publisherdep.notify(); }}); }Copy the code

The deP publisher then fires the notify() method to notify all the observers in his observer array when the data is sent

notify: function() {  
        this.subs.forEach(function(sub) {    
          sub.update();    
        });
      }  
      
Copy the code

Since the subs contains the Watcher instance object, perform the update() and then get() so that all data bound to this data in the current page will be updated in real time.

   Watcher.prototype = {
    update: function () {
      this.get();
      if (this.nodeType == 'text') {
        this.node.nodeValue = this.value;
      }
      if (this.nodeType == 'input') {
        this.node.value = this.value; }},// Get the attribute value in data
    get: function () {
      this.value = this.vm[this.name]; // Trigger get for the corresponding attribute}}Copy the code
  1. After looping through all the nodes, finally insert the compiled code snippet into the Document.
      var dom = nodeToFragment(document.getElementById(id), this);
      After compiling, return the DOM to the app
      document.getElementById(id).appendChild(dom); 
Copy the code

About Watcher, DEP and dependency collection

Notifying all Watcher means all watcher saved by the corresponding DEP. Data hijacking, iterating through all the properties in data, creates a unique DEP for each property. When the interface is initially resolved, an instruction/expression is a watcher, so when parsing the value of the instruction/expression, it reads the property in data and fires the getter. Trigger add DEP add subscription (add watcher to dep.subs), fire setter when modify the corresponding property, fire all watcher saved by deP. The whole process of what is said above is dependent on the process of collecting, in simple terms, such as the following example we should need to inform o1 and o2 two vm instances to view the updates, text1 this data will hear “depend on the collection” “oh, there are two place to rely on my data, I change need to inform them”.

let globalObj = {
    text1: 'text1'
};

let o1 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});

let o2 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    data: globalObj
});
Copy the code

[Complete project code]