preface

This article was first published on the front-end development blog

MVVM is a necessary mode for daily front-end business development in the current era (related frameworks such as React, Vue, Angular, etc.). Using MVVM allows developers to focus more on business logic rather than dom manipulation. Although it is 9012 now, the introduction of MVVM related principles has been widely used, but for the purpose of learning the basic knowledge (vue3.0 using proxy implementation is still under development), after referring to the overall idea of vuue.js, Diy implementation of a simple proxy implementation of MVVM.

This project code has been open source on Github, the project is continuing to improve, welcome to exchange and learn, if you like, please click star!

The final result

<html>
  <body>
    <div id="app">
      <div>{{title}}</div>
    </div>
  </body>
</html>
Copy the code
import MVVM from '@fe_korey/mvvm';
new MVVM({
  view: document.getElementById('app'),
  model: {
    title: 'hello mvvm! '
  },
  mounted() {
    console.log('Main program compiled, welcome to MVVM! '); }});Copy the code

An overview of structure

  • ComplierThe module parses, collects instructions, and initializes the view
  • ObserverThe module implements data monitoring, including adding subscribers and notifying subscribers
  • ParserThe module implements the parse instruction and provides the update method of the update view of the instruction
  • WatcherModule to achieve the establishment of instruction and data association
  • DepThe module implements a subscription center that collects and triggers a subscription list of data model values

The process is as follows: Complier collects and compiles instructions, selects different Parser according to different instructions, updates the initial view according to the changes of the subscription data of Parser in Watcher. The Observer listens for changes and reports them to Watcher, who in turn reports the changes to the corresponding Parser update function for view refreshes.

Module,

Complier

  • Pass the entire data model, data, into the Observer module for data listening

    this.$data = new Observer(option.model).getData();
    Copy the code
  • Loop through the DOM, scanning and extracting all instructions for each DOM element

    function collectDir(element) {
      const children = element.childNodes;
      const childrenLen = children.length;
    
      for (let i = 0; i < childrenLen; i++) {
        const node = children[i];
        const nodeType = node.nodeType;
    
        if(nodeType ! = =1&& nodeType ! = =3) {
          continue;
        }
        if (hasDirective(node)) {
          this.$queue.push(node);
        }
        if(node.hasChildNodes() && ! hasLateCompileChilds(node)) { collectDir(element); }}}Copy the code
  • Compile each instruction, selecting the corresponding Parser

    const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
    Copy the code
  • Pass the resulting Parser to Watcher and initialize the view of the DOM node

    const watcher = new Watcher(parser);
    parser.update({ newVal: watcher.value });
    Copy the code
  • $mounted()

    this.$mounted();
    Copy the code
  • Using document fragments document. CreateDocumentFragment () instead of a real dom node fragments, upon the compile all instructions, the real dom node to be supplemented document fragments back

    let child;
    const fragment = document.createDocumentFragment();
    while ((child = this.$element.firstChild)) {
      fragment.appendChild(child);
    }
    // After parsing
    this.$element.appendChild(fragment);
    delete $fragment;
    Copy the code

Parser

  • In the Complier module compiled instructions, select a different listening parser to parse, Currently include ClassParser DisplayParser, ForParser IfParser, StyleParser, TextParser, ModelParser, OnParser, OtherParser parsing module, etc.

    switch (name) {
      case 'text':
        parser = new TextParser({ node, dirValue, cs });
        break;
      case 'style':
        parser = new StyleParser({ node, dirValue, cs });
        break;
      case 'class':
        parser = new ClassParser({ node, dirValue, cs });
        break;
      case 'for':
        parser = new ForParser({ node, dirValue, cs });
        break;
      case 'on':
        parser = new OnParser({ node, dirName, dirValue, cs });
        break;
      case 'display':
        parser = new DisplayParser({ node, dirName, dirValue, cs });
        break;
      case 'if':
        parser = new IfParser({ node, dirValue, cs });
        break;
      case 'model':
        parser = new ModelParser({ node, dirValue, cs });
        break;
      default:
        parser = new OtherParser({ node, dirName, dirValue, cs });
    }
    Copy the code
  • Different parsers provide a different view refresh function, update(), to update the DOM view

    //text.js
    function update(newVal) {
      this.el.textContent = _toString(newVal);
    }
    Copy the code
  • OnParser parses event bindings, corresponding to the Methods field in the data model

    / / https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
    el.addEventListener(handlerType, e => {
      handlerFn(scope, e);
    });
    Copy the code
  • ForParser parses arrays

    / / https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
    Copy the code
  • ModelParser supports input[text/password] & Textarea, Input [radio], INPUT [checkbox],select.

    • Data changes update form: The update method triggers an update of the form’s value, just as with any directive update view

      function update({ newVal }) {
        this.model.el.value = _toString(newVal);
      }
      Copy the code
    • Form changes update data: Listen for form change events such as input,change, and set the data model in callbacks

      this.model.el.addEventListener('input', e => {
        model.watcher.set(e.target.value);
      });
      Copy the code

Observer

  • MVVMThe core of the model is generally passedObject.definePropertyget.setMethod to listen on data ingetTo add subscribers,setNotifies the subscriber to update the view. Adopted in this projectProxyTo achieve data monitoring, there are three advantages:
    • ProxyYou can listen directly on objects instead of properties
    • ProxyYou can listen for changes in the array directly
    • ProxyThere are up to 13 methods of interception,Refer toThe disadvantage is compatibility issues and failure to passpolyfillSmooth. Refer tocompatibility
  • Pay attention toProxyIt only listens for each property of itself. If the property is an object, that object is not being listened on, so recursive listening is required
  • After listening is set, one is returnedProxyReplace the original data object
var proxy = new Proxy(data, {
  get: function(target, key, receiver) {
    // Add subscribers if conditions are met
    dep.addDep(curWatcher);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    // Notify subscribers if conditions are met
    dep.notfiy();
    return Reflect.set(target, key, value, receiver); }});Copy the code

Watcher

  • In the Complier module, the instruction of each parsed Parser is directly bound to the data model, and the get listener of the Observer is triggered to add the subscriber (Watcher).

    this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
    Copy the code
  • As the data model changes, It triggers -> Observer set listener -> Dep’s Notfiy method (notifies subscribers of all subscription lists) -> Execute all Watcher update methods on subscription lists -> execute corresponding Update on Parser -> Finish updating the view

  • The set method in Watcher is used to set the bidirectional binding value. Note the access level

Dep

  • MVVMThe subscription center, where a subscription list for each property of the data model is collected
  • Includes adding subscribers, notifying subscribers and other methods
  • It’s essentially a publish/subscribe model
class Dep {
  constructor() {
    this.dependList = [];
  }
  addDep() {
    this.dependList.push(dep);
  }
  notfiy() {
    this.dependList.forEach(item= >{ item.update(); }); }}Copy the code

Afterword.

At present, the MVVM project only realizes the functions of data binding and view update. Through the implementation of this simple wheel, the dom operation, proxy, publish and subscribe mode and other basic knowledge are understood again, so as to make up for omissions. At the same time, welcome to discuss and exchange, we will continue to improve!