MVVM

I have made a detailed annotation and mind map picture version of the following code, which is handwritten for the core parts of command parsing, dependency collection, attribute monitoring, update view, etc. If you need a mind map, please contact the blogger and do not print (front small white) if you do not like it.

1. First of all, the function of MVue is to use Compile class to parse such instructions as V-text V-model and difference expressions to make the data normally displayed on the page. Updater class is used to initialize the view and new Watcher to facilitate the subsequent view update as new So Watcher is going to call getOldVal but because of data hijacking there’s got to be access to get() and when we access that method we’re going to subscribe to the observer and then we’re going to release it so that multiple observers don’t look at the same data 2. Use defineProperty in the Observer to hijack the data to see if it has changed, When the data changes, we need to do the following two things: (1) create an observer for each data and add it to the Dep (create an observer and add a subscription) (2) then use the subscription to tell the corresponding observer what data has changed (3) The observer receives notification to update the view The updater class updates the view based on whether new values are generated

So there are two steps: 1. Compile the instructions and interpolation and initialize View 2. Data hijacking is the connection between the two. When we initialize the view, we want a new Watcher, which is to bind the observer for subsequent updates, and when the data is updated, we call a callback to update the view. But because of data hijacking, when I do new Watcher I call getOldVal which means I call get and that’s where the observer is associated with the subscriber and the observer is subscribed to. Finally, the set() method is called when the data changes and the subscriber is notified and the observer is notified to update the view

A. The HTML code

<! DOCTYPEhtml>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0, the maximum - scale = 1.0, user - scalable = 0">
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h2>{{person.name}} -- {{person.age}}</h2>
      <h3>{{person.fav}}</h3>
      <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
      </ul>
      <h3>{{msg}}</h3>
      <div v-text="x" class='a'></div>
      <div v-text="person.name" class='a'></div>
      <div v-html='str'></div>
      <div v-html="person.fav"></div>
      <input type="text" v-model="msg">
      <button @click='handleClick'>@</button>
      <button v-on:click="handleClick">on</button>
      <a href="x">baidu</a>
      <a href="http://www.baidu.com">baidu</a>
    </div>
    <script src="./Observer.js"></script>
    <script src="./MVue.js"></script>
    <script>
      let vm = new MVue({
        el:"#app".data: {str:'123'.person: {name:'zcl'.age:18.fav:'computer'
          },
          msg:"Learn the principles of MVVM".x:'http://www.baidu.com'
        },
        methods: {handleClick(){
            console.log(this)}}})</script>
  </body>

</html>
Copy the code

2. Compile

const compileUtil = {
  / / tools
  getVal(expr,vm){
    // Specifically for processing data in the form of Person.name to get the real value
    return expr.split('. ').reduce((data,currentVal) = >{
      return data[currentVal]
    },vm.$data)
  },
  getContentVal(expr,vm){
    return expr.replace(/ \ {\ {(. +?) \}\}/g.(. args) = >{
      return this.getVal(args[1],vm)
    })
  },
  text(node,expr,vm){  // expr -> "x" "person.name" {{person.name}}--{{person.age}} {{person.fav}}
    // This is the point
    let value 
    if(expr.indexOf('{{')! = = -1) {// {{person.name}}--{{person.age}} {{person.fav}}
      value = expr.replace(/ \ {\ {(. +?) \}\}/g.(. args) = >{
        The replacement argument to the // replace() method can be a function rather than a string. In this case, the function is called for each match, and the string it returns will be used as replacement text. The first argument to this function is a string that matches the pattern.
        // The next argument is a string that matches the subexpression in the pattern. There can be zero or more such arguments.
        // args[1] -> person.name
        console.log(args[1])
        new Watcher(vm,args[1].() = >{
          // {{person.name}} -- {{person.age}} If replace is the case, two Watcher will be created to observe the interpolation
          // When you change the value of person.name, just update the previous {{person.name}}
          // This.updater.textupdater (node,newVal)
          Person.name Person.age
          // We can only call back one of them so if we use this method when you modify person.name or person.age we will replace the entire template
          // We only need to update the changed value, so we continue to match {{}} while updating.
          this.updater.textUpdater(node,this.getContentVal(expr,vm))
        })
        return this.getVal(args[1],vm)
      })
    }else{
      // This is not an interpolation
      value = this.getVal(expr,vm)
      new Watcher(vm,expr,(newVal) = >{
        this.updater.textUpdater(node,newVal)
      })
    }
    this.updater.textUpdater(node,value)
  },
  html(node,expr,vm){  // expr -> "x" "person.name"
    const value = this.getVal(expr,vm)
    new Watcher(vm,expr,(newVal) = >{
      this.updater.htmlUpdater(node,newVal)
    })
    this.updater.htmlUpdater(node,value)
  },
  on(node,expr,vm,eventName){ // expr -> handleClick eventName->click
    let fn = vm.$options.methods && vm.$options.methods[expr]
    node.addEventListener(eventName,fn.bind(vm),false)},setVal(expr,vm,inputVal){
    return expr.split('. ').reduce((data,currentVal) = >{ 
        data[currentVal] = inputVal
    },vm.$data)
  },
  model(node,expr,vm){
    const value = this.getVal(expr,vm)
    // Data-driven view
    new Watcher(vm,expr,(newVal) = >{
      this.updater.modelUpdater(node,newVal)
    })
    // Views drive data and then drive views
    node.addEventListener('input'.e= >{
      / / set the value
      this.setVal(expr,vm,e.target.value)
    })

    this.updater.modelUpdater(node,value)
  },
  updater: {htmlUpdater(node,value){
      node.innerHTML = value
    },
    textUpdater(node,value){
      node.textContent = value
    },
    modelUpdater(node,value){
      node.value = value
    }
  }
}

class Compile{
  constructor(el,vm){
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm 
    // Create a document fragment object to reduce page backflow and redraw
    const fragment = this.createFragment(this.el)
    // Compile the template
    this.compile(fragment)
    this.el.appendChild(fragment)
  }
  isElementNode(node){
    return node.nodeType === 1
  }
  createFragment(el){
    const f = document.createDocumentFragment()
    let firstChild
    while(firstChild=el.firstChild){
      f.appendChild(firstChild)  // appendChild adds el elements to the document fragment object and removes them!
    }
    return f
  }
  isDirective(attrName){
    return attrName.startsWith('v-')}isEventName(attrName){
    return attrName.startsWith(The '@')}compileElement(node){
    // 
    // 
      
contains v-html directives
// < button@click ='handleClick'>@ // <button v-on:click="handleClick">on</button> const attributes = node.attributes; [...attributes].forEach(attr= >{ const {name,value} = attr // name-> v-text v-html type v-model @click v-on:click if(this.isDirective(name)){ Common attributes such as ID class style do not need to be handled at template compile time const [,dirctive] = name.split(The '-') // dirctive -> text HTML model on:click const [dirName,eventName] = dirctive.split(':') // dirName -> text html model on // eventName -> click compileUtil[dirName](node,value,this.vm,eventName) node.removeAttribute('v-'+ dirctive) }else if(this.isEventName(name)){ // Handle the @click directive let [,eventName] = name.split(The '@') compileUtil['on'](node,value,this.vm,eventName) } }) } compileText(node){ {{}}} const content = node.textContent // content-> {{person.name}}---{{person.age}} {{person.fav}} if(/ \ {\ {(. +?) \} \} /.test(content)){ compileUtil['text'](node,content,this.vm) } } compile(fragment){ const childNodes = fragment.childNodes; // childNodes is used to fetch all childNodes, including element and text nodes [...childNodes].forEach(child= >{ // childNodes is a class array that needs to be converted to an array if(this.isElementNode(child)){ // Parse the element node this.compileElement(child) }else{ // Parse text nodes this.compileText(child) } if(child.childNodes && child.childNodes.length){ / / the recursion will be exposed all child nodes Similar to the < ul > < li > < / li > < / ul > < div > {{name}} < / div > this.compile(child) } }) } } class MVue{ constructor(options){ this.$options = options this.$el = options.el this.$data = options.data if(this.$el){ //1. Implement data observation new Observer(this.$data) //2 new Compile(this.$el,this)}}}Copy the code

3. The Observer

class Watcher{
  constructor(vm,expr,cb){
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.oldVal = this.getOldVal()
  }
  getOldVal(){
    Dep.target = this  // Dep is associated with Watcher
    const oldVal = compileUtil.getVal(this.expr,this.vm)
    $data. X = vm.$data. X = vm
    // We need to collect dependencies when we trigger the GET method
    // This means that we need to add observers to each data and collect those observers
    Dep.target = null  // The subs already contains the corresponding observer
    return oldVal
  }
  update(){
    // The set() method is triggered when a new value is set, where the Dep notifes the observer of the update
    const newValue = compileUtil.getVal(this.expr,this.vm)
    if(newValue! =this.oldVal){
      // Update the view again with the updater method to update the data via a callback
      this.cb(newValue)
    }
  }
}


class Dep{
  constructor(){
    this.subs = []  // Store dependencies are observers
  }
  addSub(watcher){
    // Dependent collection is collecting observers
    this.subs.push(watcher)
  }
  notify(){
    // When a new value is set, it is time for us to notify
    // Notify the corresponding observer of the view update
    this.subs.forEach(w= >w.update())
  }
}


class Observer{
  constructor(data){
    this.observe(data)
  }
  observe(data){
    if(data && typeof data === 'object') {Object.keys(data).forEach(key= >{
        this.defineReactive(data,key,data[key])
      })
    }
  }
  defineReactive(obj,key,value){
    this.observe(value)
    const dep = new Dep()
    Object.defineProperty(obj,key,{
      // Hijacking with defineProperty
      enumerable:true.configurable:false.// Cannot be deleted
      get(){
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set:(newValue) = >{ // If we use function(){}, the internal this refers to the Object, but we want the internal reference to be an Observer, so we use the arrow function to ignore the internal reference
        this.observe(newValue)
        if(newValue! =value){ value = newValue }// Notification changes
        dep.notify()
      }
    })
  }
}
Copy the code