Binding principle of Vue

MVVM Model

M: Model: Data in the corresponding data
V: View: Template
VM: ViewModel: Vue Instance Object

Decomposition Tasks

 <div id="app">
    <input type="text"  v-model="text">
    {{text}}
  </div>

To be implemented:
1. Input boxes and data binding of text nodes to data

2. When the content of the input box changes, the data in the data changes synchronously. That is, the change in view => model.

3. When the data in the data changes, the content of the text node changes synchronously. That is, the change in model => view.

To accomplish Task 1, you need to compile the DOM, and here's a point of knowledge: DocumentFragment.

1,DocumentFragment

DocumentFragment (Document Fragment) can be viewed as a node container, which can contain multiple child nodes. When we insert it into the DOM, only its child nodes will be inserted into the target node, so it is considered a container for a set of nodes. Processing nodes with DocumentFragment is much faster and better than directly operating the DOM. When Vue compiles, it hijacks (really hijacks) all the child nodes of the mounted target into the DocumentFragment, and after processing, returns the DocumentFragment as a whole to the mounted target.

var dom=nodeToFragment(document.getElementById('app'))
    console.log(dom)

    function nodeToFragment(node){
      var flag=document.createDocumentFragment();
      var child;
      while(child=node.firstChild){
        flag.append(child)  //Hijacking all child nodes in a node
      }
      return flag;
    }

2. Data Initialization Binding

function nodeToFragment(node,vm){
  var flag=document.createDocumentFragment();
  var child;
  while(child=node.firstChild){
    compile(child,vm)
    flag.append(child)  //Hijacking all child nodes in a node
  }
  
  return flag;
}

function compile(node,vm){
  //Node type is element
  if(node.nodeType===1){
    var attr=node.attributes;
    for(var i=0;i<attr.length;i++){
      if(attr[i].nodeName=='v-model'){
        var name=attr[i].nodeValue; //Gets the property name of the v-model binding
        node.value=vm.data[name];  //Assign the value of data to the node
        node.removeAttribute('v-model')
      }
    }
  }
  var reg = /\{\{(.*)\}\}/;
  //Node type is text
  if(node.nodeType===3){
    if(reg.test(node.textContent)){
      var name=RegExp.$1;  //Get the matched string
      name=name.trim(); 
      node.nodeValue=vm.data[name];//Assign the value of data to the node
    }
  }
}
function MVVM(options){
  this.data=options.data;
  var id=options.el;
  var dom=nodeToFragment(document.getElementById(id),this)
  //After compilation, return the dom to the app
  console.log(dom)
  document.getElementById(id).appendChild(dom)
}
var vm=new MVVM({
  el:'app',
  data:{
    text:'hello world'
  }
})

The code above implements Task 1, and we can see that hello world is already present in the input box and in the text node.

3. Responsive data binding

Consider the implementation of Task 2: When we enter data into an input box, we first trigger an input event (or a keyup or change event). In the corresponding event handler, we get the value of the input box and assign it to the text property of the VM instance. We will use defineProperty to hijack text in data as an accessor property of vm, so give vm.text assignment triggers the set method. There are two main things to do in the set method, the first is to update the value of the property, and the second is to leave it to task three.

function defineReactive(obj,key,val) {
  Object.defineProperty(obj,key,{
    get:function(){
      return val
    },
    set:function(newVal){
      if(newVal === val) return
      val=newVal
      console.log('set Called',val)
    }
  })
}
function observe(obj,vm) {
  Object.keys(obj).forEach(function(key){
    defineReactive(vm,key,obj[key])
  })
  
}

function MVVM(options){
  this.data=options.data;
  var data=this.data
  observe(data,this)
  var id=options.el;
  var dom=nodeToFragment(document.getElementById(id),this)
  //After compilation, return the dom to the app
  document.getElementById(id).appendChild(dom)
}

function compile(node,vm){
  //Node type is element
  if(node.nodeType===1){
    //...
   	//...{
      //...{
        var name=attr[i].nodeValue; //Gets the property name of the v-model binding
        node,addEventListener('input',function(e){
          vm[name]=e.target.value  //set of accessor properties that trigger vm
        })
        node.value=vm[name];  //Assign the value of data to the node
        node.removeAttribute('v-model')  
      }
    }
  }
  //...
  if(node.nodeType===3){
    if(reg.test(node.textContent)){
      //..
      node.nodeValue=vm[name];//Assign the value of data to the node
    }
  }
}

Task 2 is also completed, and the text property value changes synchronously with the contents of the input box (implementing view->model):

4 Implementation of bidirectional binding

Recall that whenever a new Vue is created, there are two main things to do: the first is to listen for data: observe(data), and the second is to compile HTML: nodeToFragement(id).

During data listening, a subject object dep is generated for each property in the data.
During HTML compilation, a subscriber watcher is generated for each node associated with data binding, and the watcher adds itself to the dep of the corresponding property.

We have implemented: Modify input box content=>Modify property value=>Trigger property set method in event callback function.
The next thing we want to do is notify dep.notify() =>the update method that triggers the subscriber=>Update the view.
The key logic here is how to add a watcher to the dep of the associated attribute.

function Watcher(vm,node,name){
  Dep.target=this
  this.name=name
  this.node=node
  this.vm=vm
  this.updater() 
  Dep.target=null
}
Watcher.prototype={
  updater:function() {
    this.get()
    this.node.nodeValue=this.value
  },
  //Getting attribute values in data
  get:function(){
    this.value=this.vm[this.name]
  }
}

First, Watcher assigns himself to a global variable, Dep.target;
Secondly, the update method is executed, and then the get method is executed. The get method reads the accessor properties of the vm, thereby triggering the get method of the accessor properties. The get method adds the watcher to the dep of the corresponding accessor properties.
Again, get the value of the property and update the view.
Finally, set Dep.target to null. Because it is a global variable and the only bridge that watcher associates with dep, Dep.target must have a single value at all times.

function Dep(){
  this.subs=[]
}
Dep.prototype={
  addSub:function(sub){
    this.subs.push(sub)
  },
  notify:function(){
    this.subs.forEach(function(sub){
      sub.update()
    })
  },
}

Two-way binding has been implemented so far

summary

After sorting out, in order to achieve the two-way binding of mvvm, the following points must be achieved:
1. Implement a data listener Observer that can listen on all properties of a data object, get the latest value and notify subscribers if there is a change
2. Implement an instruction parser Compile that scans and parses instructions for each element node, replaces data according to the instruction template, and binds corresponding update functions
3. Implement a Watcher that serves as a bridge between Observer and Compile to update the view by subscribing to and receiving notifications of each property change and executing the corresponding callback functions for the binding of instructions
4. mvvm entry function, integrating the above three

Keywords: Front-end Vue Vue.js

Added by lessthanthree on Mon, 07 Mar 2022 19:55:00 +0200