Using proxy to implement a simple and complete MVVM Library

Keywords: Javascript Fragment github Vue Attribute

Preface

This article was first published in Front-end Development Blog

mvvm is a necessary mode of daily business development at the front end of the current era (such as react, vue, angular, etc.). Using mvvm, developers can concentrate more on business logic than on how to operate dom. Although it has been 9012 years now, the introduction of the related principles of mvvm has been rotten, but for the purpose of learning basic knowledge (vue3.0 implemented by proxy is still under development), after referring to the overall idea of vue.js before, I have realized a simple mvvm realized by proxy.

This project code has been source in github The project is continually improving. Welcome to exchange and study. If you like, please order a star.

Final effect

<html>
  <body>
    <div id="app">
      <div>{{title}}</div>
    </div>
  </body>
</html>
import MVVM from '@fe_korey/mvvm';
new MVVM({
  view: document.getElementById('app'),
  model: {
    title: 'hello mvvm!'
  },
  mounted() {
    console.log('Host compilation completed,Welcome to use MVVM!');
  }
});

Structural overview

  • Complier module implements parsing, collecting instructions, and initializing views
  • Observer module implements data monitoring, including adding subscribers and notifying subscribers
  • Parser module implements parsing instructions and provides a new way to update views of the instructions
  • Watcher Module Implements the Association of Instructions and Data
  • Dep module implements a subscription center responsible for collecting and triggering subscription lists for each value of the data model

The process is as follows: Complier collects and compiles instructions, chooses different Parsers according to different instructions, and updates the initial view according to the changes of Parser's subscription data in Watcher. Observer monitors data changes and notifies Watcher, who then notifies the update refresh function in the corresponding Parser to refresh the view.

Module Details

Complier

  • Import the whole data model data into Observer module for data monitoring

    this.$data = new Observer(option.model).getData();
  • The whole dom is traversed through a loop, and all instructions of each dom element are scanned and extracted.

    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);
        }
      }
    }
  • Compile each instruction and select the corresponding parser Parser

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

    const watcher = new Watcher(parser);
    parser.update({ newVal: watcher.value });
  • After all instructions are parsed, the MVVM compilation is triggered to complete the callback of $mounted()

    this.$mounted();
  • The document fragment document. createDocument Fragment () is used to replace the real dom node fragment. After compiling all instructions, the document fragment is appended back to the real dom node.

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

Parser

  • After compiling the instructions in Complier module, choose different auditory parsers to parse. At present, they include ClassParser, Display Parser, ForParser, IfParser, Style Parser, TextParser, ModelParser, OnParser, Other Parser and other parsing modules.

    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 });
    }
  • Different parsers provide different view refresh functions update(), update the dom view through update

    //text.js
    function update(newVal) {
      this.el.textContent = _toString(newVal);
    }
  • OnParser Resolve event binding with data model methodsField correspondence

    //See https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts for details.
    el.addEventListener(handlerType, e => {
      handlerFn(scope, e);
    });
  • ForParser parsing array

    // See https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts for details.
  • ModelParser resolves bidirectional binding. At present, it supports input [text/password] & textarea, input [radio], input [checkbox], select four cases of bidirectional binding, double binding principle:

    • Data Change Update Form: Like other instructions to update views, the value of the update form is triggered by the update method

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

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

Observer

  • The core of MVVM model is to monitor the data through the get and set method of Object.defineProperty. Subscribers are added to get, and the subscribers are notified to update the view in set. In this project, Proxy is used to implement data monitoring, which has three advantages:

    • Proxy can listen directly to objects rather than attributes
    • Proxy can monitor array changes directly
    • Proxy has up to 13 interception methods. consult
      The disadvantage is compatibility and can not be smoothed by polyfill. consult Compatibility
  • Note that Proxy only listens to each of its attributes. If the attribute is an object, the object will not be listened on, so it needs to be listened on recursively.
  • After setting up the listener, return a Proxy instead of 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);
  }
});

Watcher

  • In the Complier module, each parsed Parser is directly bound to the data model and triggers Observer's get listener to add a Watcher.

    this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
  • When the data model changes, it triggers - > Observer's set listener - > Dep's notfiy method (notifying subscribers of all subscription lists) - > Update method of executing all Watcher s of subscription list - > Perform update - > Complete update view of corresponding Parser
  • The set method in Watcher is used to set bidirectional binding values, paying attention to the access level

Dep

  • MVVM's subscription center, where subscription lists for each attribute of the data model are collected
  • Includes methods such as adding subscribers, notifying subscribers, etc.
  • Essentially a publish/subscribe model
class Dep {
  constructor() {
    this.dependList = [];
  }
  addDep() {
    this.dependList.push(dep);
  }
  notfiy() {
    this.dependList.forEach(item => {
      item.update();
    });
  }
}

Epilogue

At present, the mvvm project only realizes the function of data binding and view updating. Through the implementation of this simple wheel, we have re-understood some basic knowledge such as dom operation, proxy, publishing and subscribing mode and so on. At the same time, we welcome you to discuss and exchange, and will continue to improve later!

Posted by phpnewbie81 on Tue, 10 Sep 2019 01:59:23 -0700