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 modelmethods
Field 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!