Recommended reading:
Implement very simple js two-way data binding
MVVM is a very popular development model for the web front-end. Using MVVM can make our code focus more on dealing with business logic rather than caring about DOM operations. Currently, the famous MVVM frameworks include vue, avalon, react, etc. These frameworks have their own advantages, but the implementation idea is roughly the same: data binding + view refresh. Out of curiosity and a willingness to struggle, I also wrote the simplest MVVM library (mvvm.js) along this direction, with a total of more than 2,000 lines of code. The naming and usage of instructions are similar to vue. Here I will share the principles of implementation and my code organization ideas.
Ideas sorting
MVVM is conceptually a pattern that truly separates views from data logic, and ViewModel is the focus of the entire pattern. To implement ViewModel, you need to associate the data model (Model) and the view (View). The entire implementation idea can be simply summarized into 5 points:
Implement a Compiler to scan and extract instructions for each node of an element;
Implement a Parser to parse the instructions on the element, which can update the intent of the instruction to the dom through a refresh function (a module that may be specifically responsible for view refreshing may be required in the middle). For example, when parsing the node <p v-show="isShow"></p>, first obtain the value of isShow in the Model, and then change node.style.display according to isShow to control the display and hiding of the elements;
Implementing a Watcher can link the refresh function of each instruction in Parser with the fields corresponding to the Model;
Implement an Observer to monitor the value change of all fields of the object, and once the change occurs, the latest value can be obtained and the notification callback can be triggered;
Use Observer to establish a monitoring of the Model in the Watcher. When a value in the Model changes, the monitoring is triggered. After the Watcher gets the new value, it calls the refresh function associated in step 2, which can achieve the purpose of refreshing the view while data changes.
Effect example
First, let’s take a look at the final usage example, which is similar to the instantiation of other MVVM frameworks:
<div id="mobile-list"><h1 v-text="title"></h1><ul><li v-for="item in brands"><b v-text="item.name"></b><span v-show="showRank">Rank: {{item.rank}}</span></li></ul></div>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'); // => <h1>Top 3 Mobile Rank List</h1>Module division
I divided MVVM into five modules to implement: compilation module Compiler, parser, view refresh module Updater, data subscription module Watcher, and data listening module Observer. The process can be briefly described as: After Compiler compiles the command, the instruction information is handed over to the parser for parsing. The Parser updates the initial value and subscribes to the Watcher for changes in data. Observer monitors the data changes and then feeds it back to the Watcher. The Watcher notifies the Updater of the change result and finds the corresponding refresh function to refresh the view.
The above process is shown in the figure:
The following is a description of the basic principles of the implementation of these five modules (the code is only posted on the key parts, please go to my Github for the complete implementation)
1. Compile module Compiler
The responsibility of Compiler is mainly to scan and extract instructions for each node of the element. Because the compilation and parsing process will traverse the entire node tree many times, in order to improve compilation efficiency, in the MVVM constructor, first convert element into a copy of document fragments. The compilation object is this document fragment and should not be a target element. After all nodes are compiled, the document fragment is added back to the original real node.
vm.complieElement implements scanning and instruction extraction of all nodes of the element:
vm.compileElement = function(fragment, root) {var node, childNodes = fragment.childNodes;// Scan the child node for (var i = 0; i < childNodes.length; i++) {node = childNodes[i];if (this.hasDirective(node)) {this.$unCompileNodes.push(node);}// Recursively scan the child node of the child node if (node.childNodes.length) {this.compileElement(node, false);}}// Scan the child nodes if (root) if (root) {this.compileAllNodes();}}The vm.compileAllNodes method will compile each node in this.$unCompileNodes (leave the instruction information to Parser). After compiling a node, it will be removed from the cache queue. At the same time, check this.$unCompileNodes.length When length === 0, it means that all compilation is completed. You can append document fragments to the real node.
2. Instruction parsing module Parser
When the compiler compiler extracts the instructions from each node, it can be sent to the parser for parsing. Each instruction has a different parsing method. All instructions only need to do two things: one is to update the data value to the view (initial state), and the other is to subscribe the refresh function to the Model's change monitoring. Here we use parsing v-text as an example to describe the general analysis method of a directive:
parser.parseVText = function(node, model) {// Get the initial value defined in the Model var text = this.$model[model];// Update the text node.textContent = text;// Corresponding refresh function: // updater.updateNodeTextContent(node, text);// Subscribe to the change of model in the watcher 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 model and the other is the callback function. The callback function is triggered through Observer. The new value last and old value old are passed in the parameter. After the Watcher gets the new value, it can find the corresponding callback (refresh function) of the model to update the view. Model and refresh functions are a one-to-many relationship, that is, a model can have as many callback functions (refresh functions) that handle it, for example: v-text="title" and v-html="title" instructions share a data model field.
Add data subscription to watcher.watch implementation method is:
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 callbacks if (!callbacks[field]) {callbacks[field] = [];}// Cache callbacks[field].push([callback, context]);}When the field field of the data model changes, Watcher triggers all callbacks in the cache array that have subscribed to the field.
4. Data monitoring module Observer
Observer is the core foundation of the entire mvvm implementation. I read an article saying that Oo (Object.observe) will ignite the data binding revolution and bring huge influence to the front-end. Unfortunately, the ES7 draft has abandoned Oo! There is no browser support at present! Fortunately, there is also Object.defineProperty that can simulate a simple Observer by intercepting the access descriptors (get and set) of object properties:
// Intercept the get and set methods of the prop property of the object Object.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 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);// Trigger callback self.triggerChange(this, method);return result;});});array.__proto__ = arrayMethods;}This implementation method is referenced from vue. I think it is used very well, but the length attribute of the array cannot be listened to, so in MVVM, the array.length operation should be avoided
5. View refresh module Updater
Updater is the easiest among the five modules, you only need to be responsible for the refresh function corresponding to each instruction. After a series of tossing and leaving the final result to the Updater for view or event updates, for example, the refresh function of v-text is:
updater.updateNodeTextContent = function(node, text) {node.textContent = text;}Refresh function of v-bind:style:
updater.updateNodeStyle = function(node, property, value) {node.style[propperty] = value;}Implementation of two-way data binding
Bidirectional data binding of form elements is one of the biggest features of MVVM:
In fact, the implementation principle of this magical function is also very simple. There are only two things to do: one is to update the form value when the data changes, and the other is to update the data when the form value changes, so that the data value is bound to the form value.
Data changes to update form values can be easily done using the Watcher module mentioned above:
watcher.watch(model, function(last, old) {input.value = last;});'To update data in form changes, you only need to listen to the form's worth changing events 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 radio, checkbox and select are the same principle.
The above, the entire process and the basic implementation ideas of each module have been explained. The first time I posted an article in the community, my language expression skills are not very good. If there are any wrong words and bad things written, I hope everyone can criticize and correct them!
Conclusion
I'm trying this simple mvvm.js because I used vue.js in my framework project, but I just used its instruction system. A lot of functions were only used about a quarter. I thought it would be enough to just implement data-binding and view-refresh. As a result, I didn't find such a javascript library, so I created such a wheel myself.
Although the functions and stability are far less than popular MVVM frameworks such as vue, and the code implementation may be relatively rough, a lot of knowledge has been added by building this wheel~ Progress lies in tossing!
At present, my mvvm.js only implements the most basic function. I will continue to improve and strengthen it in the future. If you are interested, please discuss and improve it together~