MVVM is a very popular development model for Web Front-end. MVVM allows our code to focus more on processing business logic than on DOM operations. Next, I will introduce JavaScript to use data binding to implement a simple MVVM library. If you are interested, let's learn it together.
Recommended reading:
Implement simple js two-way Data Binding
MVVM is a very popular development model for Web Front-end. MVVM allows our code to focus more on processing business logic than on DOM operations. Currently, the well-known MVVM frameworks include vue, aveon, and react. These frameworks have their own merits, but the implementation idea is basically the same: Data Binding + view refresh. Out of curiosity and willingness to work, I wrote a simple MVVM Library (mvvm. js), a total of more than 2000 lines of code, command naming and usage are similar to vue. Here we will share the implementation principles and my code organization ideas.
Train of Thought
In terms of concept, MVVM truly separates views from data logic. ViewModel is the focus of the entire mode. To implement ViewModel, You need to associate the data Model with the View. The overall implementation idea can be summarized into five points:
A Compiler scans and extracts commands from each node of an element;
Implement a Parser to parse the commands on the element, and update the command intent to the dom through a refresh function (a module responsible for refresh the view may be required in the middle), such as parsing nodes
First obtain the isShow value in the Model, and then change node. style. display according to isShow to control the display and hiding of elements;
Implement a Watcher to associate the refresh function of each instruction of Parser with the field of the corresponding Model;
Implement an Observer so that all fields of the object can be monitored for value changes. Once a change occurs, the latest value can be obtained and notification callback is triggered;
The Observer creates a Model listener in Watcher. When a value in the Model changes, the listener is triggered, after obtaining the new value, Watcher calls the refresh function associated with Step 2 to refresh the view while changing data.
Result example
First, let's look at the final example, which is similar to the instantiation of other MVVM frameworks:
var element = document.querySelector('#mobile-list');var vm = new MVVM(element, {'title' : 'Mobile List','showRank': true,'brands' : [{'name': 'Apple', 'rank': 1},{'name': 'Galaxy', 'rank': 2},{'name': 'OPPO', 'rank': 3}]});vm.set('title', 'Top 3 Mobile Rank List'); // => Top 3 Mobile Rank List
Module division
I divided MVVM into five modules for implementation: Compilation module Compiler, parsing module Parser, view refresh module Updater, data subscription module Watcher, and data listening module Observer. The process can be briefly described as follows: Compiler compiles the command and submits the command information to Parser for parsing. Parser updates the initial value and subscribes to Watcher for data changes, the Observer monitors data changes and then sends them back to Watcher. Watcher then notifies Updater of the Change result to find the corresponding refresh function to refresh the view.
The above process:
The following describes the basic principles of the implementation of these five modules (the code is only highlighted in the key part. For the complete implementation, please refer to my Github)
1. Compile the Compiler Module
Compiler is mainly responsible for scanning and extracting commands for each node of an element. Because the compilation and parsing process will traverse the entire node tree multiple times, therefore, in order to improve compilation efficiency, the MVVM constructor first converts the element into a copy in the form of file fragments. The fragment compilation object is the File fragment instead of the target element, after all the nodes are compiled, add the file fragments to the original real nodes.
Vm. complieElement scans all nodes of the element and extracts commands:
Vm. complieElement = function (fragment, root) {var node, childNodes = fragment. childNodes; // scan the subnode for (var I = 0; I <childNodes. length; I ++) {node = childNodes [I]; if (this. hasDirective (node) {this. $ unCompileNodes. push (node);} // recursively scan the sub-node if (node. childNodes. length) {this. complieElement (node, false) ;}// the scan is complete. compile all nodes containing commands if (root) {this. compileAllNodes ();}}
Vm. the compileAllNodes method applies to this. $ compile each node in unCompileNodes (give the instruction information to Parser). After compiling a node, remove it from the cache queue and check this. $ unCompileNodes. when length = 0, it indicates that all files are compiled. You can append the file fragments to the real node.
2. Instruction parsing module Parser
When the Compiler extracts the commands of each node, it can be parsed by the parser. Each Command has a different resolution method. The Parsing Method of all commands only needs to do two things: first, update the data value to the view (Initial State ), the second is to subscribe the refresh function to the change monitoring of the Model. The following uses parsing v-text as an example to describe the general Parsing Method of a command:
Parser. parseVText = function (node, model) {// obtain the initial value var text = this defined in the Model. $ model [model]; // update the node text node. textContent = text; // the corresponding refresh function: // updater. updateNodeTextContent (node, text); // subscribe to watcher for model changes in watcher. watch (model, function (last, old) {node. textContent = last; // updater. updateNodeTextContent (node, text );});}
3. Data subscription module Watcher
In the previous example, Watcher provides a watch method to subscribe to data changes. One parameter is the model field, and the other is the callback function. The callback function is triggered by Observer, input the new value last and old value old. After Watcher obtains the new value, it can find the callback (refresh function) corresponding to the model to update the view. The relationship between the model and the refresh function is one-to-many, that is, a model can have any number of callback functions (refresh functions) that process it, such: the commands v-text = "title" and v-html = "title" share a data model field.
Add watcher. watch as follows:
Watcher. watch = function (field, callback, context) {var callbacks = this. $ watchCallbacks; if (! Object. hasOwnProperty. call (this. $ model, field) {console. warn ('the field: '+ field + 'does not exist in model! '); Return;} // create an array of cache callback functions if (! Callbacks [field]) {callbacks [field] = [];} // cache callback function callbacks [field]. push ([callback, context]);}
When the field of the data model changes, Watcher will trigger the cache array to subscribe to all the callbacks of the field.
4. Data listening module Observer
Observer is the core basis for the entire mvvm implementation. I have read an article about O. o (Object. it will trigger the Data Binding revolution and bring huge influence to the front-end. o is abandoned! No browser support currently! Fortunately, Object. defineProperty can simulate a simple Observer by intercepting the access Descriptor (get and set) of Object attributes:
// Intercept the get and set methods of the object's prop attributes. defineProperty (object, prop, {get: function () {return this. getValue (object, prop) ;}, set: function (newValue) {var oldValue = this. getValue (object, prop); if (newValue! ==Oldvalue) {this. setValue (object, newValue, prop); // trigger the change callback this. triggerChange (prop, newValue, oldValue );}}});
Then there is another question: how to monitor Array Operations (push, shift, etc? All MVVM frameworks are implemented by rewriting the prototype of the array:
Observer. rewriteArrayMethods = function (array) {var self = this; var arrayProto = Array. prototype; var arrayMethods = Object. create (arrayProto); var methods = 'push | pop | shift | unshift | splice | sort | reverse '. split ('|'); methods. forEach (function (method) {Object. defineProperty (arrayMethods, method, function () {var I = arguments. length; var original = arrayProto [method]; var args = new Array (I); while (I --) {args [I] = arguments [I];} var result = original. apply (this, args); // triggers the callback self. triggerChange (this, method); return result;}) ;}); array. _ proto _ = arrayMethods ;}
This implementation method is referenced from vue and is very useful. However, the length attribute of the array cannot be monitored. Therefore, you should avoid the operation of array. length in MVVM.
5. View refresh module Updater
Updater is the simplest of the five modules. You only need to be responsible for the refresh function corresponding to each instruction. After a series of hard work, the other four modules handed over the final results to the Updater for view or event update. For example, the refresh function of v-text is:
updater.updateNodeTextContent = function(node, text) {node.textContent = text;}
V-bind: style refresh function:
updater.updateNodeStyle = function(node, propperty, value) {node.style[propperty] = value;}
Implementation of bidirectional data binding
Two-way Data Binding of form elements is one of the biggest features of MVVM:
In fact, this magic function is easy to implement. There are only two things to do: update the form value when the data changes, and update the data when the form value changes, in this way, the data value is associated with the form value.
The Data Change update form value can be easily implemented using the Watcher module described above:
watcher.watch(model, function(last, old) {input.value = last;});'
To update data with form changes, you only need to monitor the event worthy of form changes in real time and update the corresponding fields of the data model:
var model = this.$model;input.addEventListenr('change', function() {model[field] = this.value;});‘
Other forms, such as radio, checkbox, and select, share the same principle.
As mentioned above, the entire process and the basic implementation ideas of each module have been completed. For the first time, I published an article in the community, and the language expression ability is not very good. If something is wrong, I hope you can correct your criticism!
Conclusion
Toss this simple mvvm. js is because vue is used in its own framework project. javascript only uses its command system, and a lot of functions only use about 1/4. It is enough to realize data-binding and view-refresh. The result is that such a javascript library is not found, so I made such a wheel myself.
Although the features and stability are far inferior to those of popular MVVM frameworks such as vue, the code implementation may be rough, but this wheel has increased a lot of knowledge ~ Progress lies in tossing!
Currently, my mvvm. js only implements the most basic functions. I will continue to improve and make it robust in the future. If you are interested, please join us in exploring and improving it ~