Vue framework is a typical FRONT-END framework of MVVM (Model-View-ViewModel) mode. Its biggest characteristic is that two-way data binding is done between View and ViewModel. When the Model changes, the View binding Model data will change accordingly. When the View changes, so does the corresponding Model.

Let’s now implement a small and simple MVVM framework ~ preliminary file structure design is as follows:

index.html

Page framework, create MVVM instances, mount page elements and data. Js, observer.js, compile. Js, and MVVM.

<! DOCTYPEhtml>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>My MVVM</title>
</head>

<body>
  <div id="app">
    <! -- Two-way data binding -->
    <input type="text" v-model='msg'> {{msg}}
  </div>
</body>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
  // How to implement MVVM?
  // Implement bidirectional binding in Vue 1. Template compilation 2. Data hijacking 3
  let vm = new MVVM({
    el: '#app'.//el:document.getElementById('app')
    data: {
      msg: 'hello'}})</script>

</html>
Copy the code

The following JS code with detailed comments, while reading the article while hands-on effect is better

mvvm.js

MVVM. Js defines the MVVM class, which first mounts all available attributes to the instance. If there is a template to compile, start compiling, involving data hijacking, data proxy, template compilation three stages. Among them, data hijacking is to change all the attributes of the object to GET, set; $this.$data (MVVM); $this.$data (MVVM); $this. The template compilation stage is to compile with data and elements and return a page with complete data content.

class MVVM {
  constructor(options) {
    // Mount the available stuff to the instance first
    this.$el = options.el
    this.$data = options.data

    // Start compiling if there is a template to compile
    if (this.$el) {
      new Observer(this.$data)  // Data hijacking, change all attributes of the object to get, set
      this.proxyData(this.$data)
      new Compile(this.$el, this)  // Compile with data and elements}}// Proxy data, because the user might want the this. MSG value instead of this.$data. MSG value
  proxyData(data) {
    Object.keys(data).forEach(key= > {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(newVal) {
          data[key] = newVal
        }
      })
    })
  }
}
Copy the code

compile.js

Compile. Js combines data with page elements and returns a complete page with the corresponding data content for browser rendering. If a template exists, the following steps are required:

  1. Move the real DOM into memory and place it in the fragment (node2Fragment(el)Function)
  2. Compile (The compile (fragments) functions), extract element node and text node, implement different compilation methods for element node and text node:
    • If it is an element node, you need to compile the element node first (compileElement(node)Function), and recurse
    • If it is a text node, compile the text node directly (compileText(node)Function), extract the contents of {{}} for data padding
  3. Put the compiled Element back on the page

class Compile {
  constructor(el, vm) {
    // Determine if el is an element node. If it is an HTML element node, return it directly. Otherwise, use Document. querySelector to find the EL node and return it
    this.el = this.isElementNode(el) ? el : document.querySelector(el)
    this.vm = vm
    if (this.el) {
      // 1. Move the real DOM into memory and place it in the fragment
      let fragment = this.node2Fragment(this.el)
      // 2. Compile -- Extract the desired element node and text node v-model {{}}
      this.compile(fragment)
      // 3. Put the compiled element back on the page
      this.el.appendChild(fragment)
    }
  }

  // Auxiliary methods

  isElementNode(node) {
    return node.nodeType === 1
  }
  isDirective(name) {
    return name.includes('v-')}// Core method

  // Put all el elements into memory
  node2Fragment(el) {
    let fragment = document.createDocumentFragment() // Document fragments
    let firstChild
    while (firstChild = el.firstChild) {
      fragment.appendChild(firstChild)
    }
    return fragment
  }

  / / compile
  compile(fragment) {
    // childNodes cannot get nested childNodes and needs to use recursion
    let childNodes = fragment.childNodes
    Array.from(childNodes).forEach(node= > {
      // Element node
      if (this.isElementNode(node)) {
        this.compileElement(node)
        this.compile(node) // Use recursion
      } else {
        // Text node
        this.compileText(node)
      }
    })
  }

  // Compile elements v-model, v-text, etc
  compileElement(node) {
    let attrs = node.attributes
    Array.from(attrs).forEach(attr= > {
      // Check whether the attribute name contains v-
      let attrName = attr.name
      if (this.isDirective(attrName)) {
        let expr = attr.value //expr is the value of the instruction
        // node this.vm.$data expr
        // Take the name after v-, such as v-model model, v-text text, etc
        // let type = attrName.slice(2)
        let [, type] = attrName.split(The '-')
        Util[type](node, this.vm, expr)
      }
    })
  }

  // Compile text, {{}}
  compileText(node) {
    let expr = node.textContent
    let reg = /\{\{([^}]+)\}\}/g / / match {{}}
    if (reg.test(expr)) {
      const type = 'text'
      Util[type](node, this.vm, expr)
    }
  }
}

Util = {
  // Get the corresponding data on the instance, such as msg.a.b=>'hello'
  // msg.a.b=>this.$data.msg=>this.$data.msg.a=>this.$data.msg.a.b
  getVal(vm, expr) {
    expr = expr.split('. ')
    return expr.reduce((prev, next) = > {
      return prev[next]
    }, vm.$data)
  },
  {{MSG}}=>'hello'
  getTextVal(vm, expr) {
    return expr.replace(/\{\{([^}]+)\}\}/g.(.arguments) = > {
      // arguments[1] is regular to match parenthesis content, such as {{MSG}} MSG
      return this.getVal(vm, arguments[1])})},/ / assignment
  // For example, if msg.a.b is assigned a new value, then value is assigned at the end
  setVal(vm, expr, value) {
    expr = expr.split('. ')

    return expr.reduce((prev, next, curIndex) = > {
      if (curIndex === expr.length - 1) {
        return prev[next] = value
      }
    }, vm.$data)
  },
  // Text processing
  text(node, vm, expr) {
    let updateFn = this.update['textUpdater']

    // get {{a}}{{b}} a, b
    expr.replace(/\{\{([^}]+)\}\}/g.(.arguments) = > {
      new Watcher(vm, arguments[1].newVal= > {
        // If the data changes, the text node needs to retrieve the dependent data to update the text node
        updateFn && updateFn(node, this.getTextVal(vm, expr))
      })
    })

    let value = this.getTextVal(vm, expr)
    updateFn && updateFn(node, value)
  },
  // Input box processing
  model(node, vm, expr) {
    let updateFn = this.update['modelUpdater']
    // There should be a monitor here. When data changes, watcher's callback should be called to pass in the new value
    new Watcher(vm, expr, newVal= > {
      updateFn && updateFn(node, this.getVal(vm, expr))
    })
    updateFn && updateFn(node, this.getVal(vm, expr))
    node.addEventListener('input'.e= > {
      let newVal = e.target.value
      this.setVal(vm, expr, newVal)
    })
  },
  update: {
    // Text update
    textUpdater(node, value) {
      node.textContent = value
    },
    // Input box updated
    modelUpdater(node, value) {
      node.value = value
    }
  }
}
Copy the code

observer.js

Observer. js changes all the bound data in the page to response. that is to say, the original attributes of the data data are changed to get and set forms. In the defineReactive function, we created a new instance of Dep for each data. Dep is a typical class for publishing subscriptions (see watcher.js below), which can be used to add subscriber information and trigger data updates. In this function, data hijacking is done using Object.defineProperty, so that when the data changes (for a set), all subscribers of the data are notified that the data has changed and the corresponding subscribers are told to update the data.

class Observer {
  constructor(data) {
    this.observe(data)
  }

  // Change the data attributes to get and set
  observe(data) {
    if(! data ||typeofdata ! = ='object') return
    Object.keys(data).forEach(key= > {
      // Start hijacking
      this.defineReactive(data, key, data[key])
      // If the object is hijacked, the attributes within the object are also hijacked
      this.observe(data[key])
    })
  }

  // Define the response
  defineReactive(data, key, value) {
    let _this = this
    let dep = new Dep() // Each change corresponds to an array that holds all the updated operations
    Object.defineProperty(data, key, {
      enumerable: true.configurable: true.get() {
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newValue) {
        if(newValue ! == value) {// When setting a new value, you still need to hijack it if it is an object
          _this.observe(newValue)
          value = newValue
          dep.notify() // Notify all subscribers of the data change}})}}Copy the code

watcher.js

Watcher.js defines the observer class, which is used to add observers to dom elements that need to be changed. The new value is compared with the old value, and if it changes, the corresponding method (such as updating the page) is executed.

class Watcher {
  constructor(vm, expr, cb) {
    this.vm = vm
    this.expr = expr
    this.cb = cb
    this.value = this.get()
  }

  // Get the corresponding data on the instance, such as msg.a.b=>'hello'
  getVal(vm, expr) {
    expr = expr.split('. ')
    return expr.reduce((prev, next) = > {
      return prev[next]
    }, vm.$data)
  }

  get() {
    Dep.target = this
    let value = this.getVal(this.vm, this.expr)
    Dep.target = null
    return value
  }

  // External exposure method
  update() {
    let newVal = this.getVal(this.vm, this.expr)
    let oldVal = this.value
    if(newVal ! == oldVal) {this.cb(newVal)
    }
  }
}

// Publish a subscription
class Dep {
  constructor() {
    // Subscribe to the array
    this.subs = []
  }

  // Add a subscription
  addSub(watcher) {
    this.subs.push(watcher)
  }

  notify() {
    this.subs.forEach(watcher= > {
      watcher.update()
    })
  }
}
Copy the code

Testing the HTML page, the MVVM can be successfully implemented, and the View and Model are bidirectional bound. Done ~!