300 lines of code to realize the MVVM responsive principle of Vue

preface

Source download

Vue's responsive principle is an old interview question, and most people will choose to recite the answer directly to deal with the interview. Once the interviewer continues to ask questions, he can't answer anything. Therefore, I hope to write code (about 300 lines) to implement a simple Vue MVVM framework by referring to Vue source code, so as to let us better understand Vue's responsive principle.

We'll pass

  • Implement instruction parser Compile
  • Implement data listener Observer
  • Implement Watcher

To realize the responsive principle of MVVM of the whole Vue

The functions implemented are not perfect. If you are interested, you can add them yourself. This code is mainly to complete the whole process of the responsive principle, understand what Observer, Compile and Watcher are, what they are used for, and how to build a bridge of the whole MVVM architecture through these three, so as to realize the responsive principle of Vue.

Create a simple html as a test

<div id="app">
  <div>{{person.name}} -- {{person.age}}</div>
  <div>{{person.fav}}</div>
  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul>
  <div v-html="htmlStr" v-bind:style="{backgroundColor:red}"></div>
  <div v-text="person.fav"></div>
  <!-- <input type="text" v-bind:value='msg'> -->
  <input type="text" v-model='msg'>
  <div>{{msg}}</div>
  <h2 v-text="tip" v-on:click="clickMe"></h2>
</div>

Like Vue, create a Vue object in the script tag below

// To avoid conflicts with Vue, MVue is used as the class name
const app = new MVue({
    el: "#app",
    data: {
      person: {
        name: 'GHkmmm',
        age: '21',
        fav: 'programming'
      },
      msg: 'understand Vue Bidirectional binding principle of',
      tip: 'Point me',
      htmlStr: 'hello world!'
    },
    methods: {
      clickMe(){
        console.log('click');
      }
    }
  })

Create MVue class

Used to receive incoming parameters

class MVue{
  constructor(options){
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;

    if(this.$el){
      // Implement an instruction parser
      new Compile(this.$el, this);
    }
  }
}

Implement an instruction parser Compile

class Compile{
  constructor(el, vm){
    this.el = this.isElementNode(el) ? el : document.querySelector(el);
    this.vm = vm;
    /*
    	When compiling the page content, it is impossible to change each object one by one because it needs to traverse each object
    	But through the form of document fragments, modify them uniformly, and then use fragment to update the whole page
    */
    // 1. Get the document fragment object and put it into memory to reduce the backflow and redrawing of the page
    const fragment = this.nodeFragment(this.el);
    // 2. Compilation template
    this.compile(fragment);
    // 3. Append child element to root element
    this.el.append(fragment);
  }
}

Get document fragments

Traverse the child nodes and add them to the document fragment object

nodeFragment(node){
  // Create document fragment object
  const f = document.createDocumentFragment();
  let firstChild;
  //Traverse the child nodes under < div id = "app" >
  while(node.firstChild){
    firstChild = node.firstChild;
    // Add the traversed node to the document fragment object
    f.append(firstChild);
  }
  // Return document fragment object
  return f;
 }

Compilation template

After the fragment is passed in, we need to traverse the nodes in the fragment to determine whether it is an element node or a text node. Because the processing methods of the two are different, we need to use different methods to compile

compile(fragment){
  	// Pass in the just obtained document fragment object fragment
    // 1. Get child nodes
    const childNodes = fragment.childNodes;
  	// Traversing the childNodes array
    [...childNodes].forEach(child => {
      /*
      	isElementNode(el){
          return el.nodeType === 1; 
        }
      */
      if(this.isElementNode(child)){
        // Element node
        // console.log('element node ', child);
        this.compileElement(child);
      }else{
        // Non element node (text node...)
        // console.log('text node ', child);
        this.compileText(child);
      }
      // Judge whether there are any child nodes under the child node. If so, recursion is performed
      if(child.childNodes && child.childNodes.length){
        this.compile(child);
      }
    })
  }

Compile element node

compileElement(node){
  	// Get all attributes on the node
    const attributes = node.attributes;
  	// Traversal property array
    [...attributes].forEach(attr => {
      // Using deconstruction assignment, the name attribute and value attribute in attr are extracted separately
      // The name and value here correspond to the attributes in attr and cannot be changed
      // name: v-text,v-model,v-bind....
      // vaule: v-text,v-model.. Value followed
      const { name, value } = attr;
      /*
      	Judge whether the name starts with 'v -'. If so, compile it. If not, do not process it
      	isDirective(attrName){
          return attrName.startsWith('v-');
        }
      */
      if(this.isDirective(name)){
        // According to the '-' split name, the split method returns an array. The first parameter is not used, and the second parameter is assigned to directive
        // The directive here is different from the above. Just choose a name here
        const [,directive] = name.split('-');
        // Because the instructions may be not only v-text, but also v-on:click, further segmentation is required
        const [dirName,eventName] = directive.split(':');
        // Update data driven view
        // Call different methods in compleleutil according to different dirnames
        // dirName refers to text,html,bind,on
        complileUtil[dirName](node, value, this.vm, eventName)

        // Delete attributes on tags with instructions
        node.removeAttribute(name);
      }
    })
  }

Compile text node

compileText(node){
  // Get text content
  const content = node.textContent;
  if(/\{\{(.+?)\}\}/.test(content)){
    complileUtil['text'](node,content,this.vm)
  }
}

Compleleutil object

The processing methods of different instructions are implemented internally

const complileUtil = {
  getVal(expr, vm){
    /*
    	expr It could be msg or person Name, so you need to split expr again
    	
    	After segmentation, an array is obtained, such as ['person ','name']. After using the reduce method
    	Return VM for the first time$ data['person']
    	Return VM for the second time$ data['person']['name']
    	
    	Another example is MSG, which gets the array ['msg '], so it directly returns VM$ Data ['msg ']
    */
    return expr.split('.').reduce((data, currentVal) => {
      // console.log(data);
      // console.log(currentVal);
      return data[currentVal];
    }, vm.$data//Initial value)
  },
  setVal(expr, vm, inputVal){
    return expr.split('.').reduce((data, currentVal) => {
      data[currentVal] = inputVal;
    }, vm.$data)
  },
  
	// Compile v-text instruction and mustache syntax of text node {{msg}}
  text(node, expr, vm){
    let value;
    // Determine whether to start with '{{'
    if(expr.indexOf("{{")!==-1){
      // The string in braces is extracted by matching double braces through regular expression
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        // Pass in the getVal method in VM$ Find data in data
        return this.getVal(args[1], vm);
      })
    }else{
      value = this.getVal(expr, vm);
    }
    // update the view
    this.updater.textUpdater(node, value);
  },
  // Compiling v-html instructions
  html(node, expr, vm){
    const value = this.getVal(expr, vm);
    this.updater.htmlUpdater(node, value);
  },
  // Compiling v-model instructions
  model(node, expr, vm){
    const value = this.getVal(expr, vm);
    // View = > data = > View
    // Listen for input events
    node.addEventListener('input', (e)=>{
      this.setVal(expr, vm, e.target.value);
    })
    this.updater.modelUpdater(node, value);
  },
  // Compiling v-on instructions
  on(node, expr, vm, eventName){
    let fn = vm.$options.methods && vm.$options.methods[expr];
    node.addEventListener(eventName, fn.bind(vm), false)
  },
  // Compiling v-model instructions
  bind(node, expr, vm, attrName){
    ...//Here you can do it yourself
  },
    
  //Updated function
  updater: {
    textUpdater(node, value){
      node.textContent = value;
    },
    htmlUpdater(node, value){
      node.innerHTML = value;
    },
    modelUpdater(node, value){
      node.value = value;
    },
    bindUpdater(node, attrName, value){
      node[attrName] = value
    }
  }
}

Implement a data listener Observer

Create Observer class

effect

Use object Define property to set getter s and setter s for all properties in data

class Observer{
  constructor(data){
    this.observer(data);
  }
  observer(data){
    if(data && typeof data === 'object'){
      // Traversal data object
      Object.keys(data).forEach(key => {
        this.defineReactive(data, key, data[key]);
      })
    }
  }
  defineReactive(data, key, value){
    // Recursive traversal
    this.observer(value);
    const dep = new Dep();
    // Listen and hijack all properties 
    // defineProperty passes in data and key to add corresponding getter and setter to data[key]
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: false, //Describes whether the attribute is configured and whether it can be deleted
      get(){
        // This function is called when the property is accessed
        // initialization
        // When the subscription data changes, add observers to Dep
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      // This function is called when the property value is modified
      // The arrow function is used here to make this point to the upper layer instead of Object
      set: (newVal) => {
        this.observer(newVal)
        if(newVal !== value){
          value = newVal;
        }
        //Tell Dep to notify changes
        dep.notify();
      }
    });
  }
}

Implemented in MVue class

class MVue{
  constructor(options){
    this.$options = options;
    this.$el = options.el;
    this.$data = options.data;

    if(this.$el){
      // Implement a data observer
      new Observer(this.$data);
      // Implement an instruction parser
      new Compile(this.$el, this);
    }
  }
}

Implement dependency collector Dep

Create Dep class

effect

  • Collect observers
  • If the hijacked data in the Observer changes, Dep will be notified to notify the corresponding Observer
class Dep{
  constructor() {
    this.subs = [];
  }
  // Collect observers
  addSub(watcher){
    this.subs.push(watcher);
  }
  // Notify observers of updates
  notify(){
    this.subs.forEach(w => w.update())
  }
}

Implement a Watcher to update the view

Create Watcher class

class Watcher{
  constructor(vm, expr, callback){
    this.vm = vm;
    this.expr = expr;
    this.callback = callback;
    this.oldVal = this.getOldVal()
  }
  getOldVal(){
    Dep.target = this;
    const oldVal = complileUtil.getVal(this.expr, this.vm)
    Dep.target = null;
    return oldVal;
  }
  update(){
    const newVal = complileUtil.getVal(this.expr, this.vm);
    if(newVal !== this.oldVal){
       this.callback(newVal);
    }
  }
}

Implement Watcher in the method of each processing instruction of compleleutil

new Watcher(vm, expr, callback)

const complileUtil = {
  getContentVal(expr, vm){
    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
      return this.getVal(args[1], vm)
    })
  },
  text(node, expr, vm){
    let value;
    if(expr.indexOf("{{")!==-1){
      value = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
        new Watcher(vm, args[1], ()=>{
          this.updater.textUpdater(node, this.getContentVal(expr,vm));
        })
        return this.getVal(args[1], vm);
      })
    }else{
      value = this.getVal(expr, vm);
    }
    this.updater.textUpdater(node, value);
  },
  html(node, expr, vm){
    const value = this.getVal(expr, vm);
    // Bind the observer. If the data changes in the future, the callback here will be triggered to update
    new Watcher(vm, expr, (newVal)=>{
      this.updater.htmlUpdater(node, newVal);
    })
    this.updater.htmlUpdater(node, value);
  },
  ...
}

Exhibition

At this point, we have realized the MVVM response of the whole Vue and the two-way binding of data

summary

vue adopts data hijacking and publisher subscriber mode
Object.defineProperty() hijacks the setter s and getter s of various properties. When the data changes, it publishes a message to the dependent collector Dep to notify the observer and make the corresponding callback function to update the view.

As the binding entry, MVVM integrates Observer,Compile and Watcher, monitors model data changes through Observer, parses and compiles template instructions through Compile, and finally uses Watcher to build
Communication bridge between observer and compile to achieve data change = > View update; Interactive view change = > bidirectional binding effect of data model change

If there are any questions in the article, you are welcome to point out

Reference tutorial: Reference tutorial

Tags: Javascript Front-end Vue Vue.js

Posted by superdezign on Wed, 25 May 2022 19:16:48 +0300