Only 30 lines of code implement MVC in Javascript

  • 2020-12-18 01:45:53
  • OfStack

Since around 2009, MVC gradually shines in the front field, and finally in 2015, with the launch of React Native, AngularJS, EmberJS, Backbone, ReactJS, RiotJS, VueJS... A rapid succession of names, some of which have faded from view, some of which are still thriving, and some of which have become isolated in their particular ecological environment. Either way, MVC has and will continue to profoundly influence the way front-end engineers think and work.

Many examples of MVC start with a concept within a specific framework, such as collection in Backbone or model in AngularJS, which is certainly a good idea. But the framework is framework, rather than a class library (jQuery) or tool set (Underscore), behind is because they have many excellent design concept and best practices, these design essence complement each other, interlocking, lack of 1, want to in a short period of time through the complex framework and see some one kind of the essence of design patterns is not an easy thing.

That's where this essay comes in -- the prototype code that is created to help you understand the concept should be as simple as possible, just simple enough to understand the concept.

1. MVC is based on the observer pattern, which is the key to achieving synchronization between model and view
For the sake of simplicity, each model instance contains only 1 primitive value value.


function Model(value) {
  this._value = typeof value === 'undefined' ? '' : value;
  this._listeners = [];
}
Model.prototype.set = function (value) {
  var self = this;
  self._value = value;
  // model The registered callback function should be notified when the value in 
  //  In accordance with the Javascript Event-Handled 1 As a general mechanism, we call the callback function asynchronously 
  //  If you feel setTimeout Impact performance can also be used requestAnimationFrame
  setTimeout(function () {
    self._listeners.forEach(function (listener) {
      listener.call(self, value);
    });
  });
};
Model.prototype.watch = function (listener) {
  //  Register the listening callback function 
  this._listeners.push(listener);
};

// html Code: 
<div id="div1"></div>
//  Logic code: 
(function () {
  var model = new Model();
  var div1 = document.getElementById('div1');
  model.watch(function (value) {
    div1.innerHTML = value;
  });
  model.set('hello, this is a div');
})();

Using the observer pattern, we have achieved in the calling model set method to change its value, the template is an update, but the implementation is very uncomfortable, because we need to manually monitor model value change (through watch method) and pass in a callback function, is there any way to let view (one or more dom node) and model easier bound?

2. Implement bind method, bind model and view


Model.prototype.bind = function (node) {
  //  will watch Logic and generic callback functions are put here 
  this.watch(function (value) {
    node.innerHTML = value;
  });
};

// html Code: 
<div id="div1"></div>
<div id="div2"></div>
//  Logic code: 
(function () {
  var model = new Model();
  model.bind(document.getElementById('div1'));
  model.bind(document.getElementById('div2'));
  model.set('this is a div');
})();

With a simple encapsulation, the binding between view and model has already begun to take shape, making it easy to implement even if multiple view bindings are required. Note that bind is a native method on prototype class Function, but it is not closely related to MVC, and The author really likes the word bind, which is concise and comprehensive in the first language, so the native method is simply covered here, so you can ignore it. Anyway, although the complexity of the binding has decreased, this step still depends on us to do it manually. Is it possible to completely decouple the binding logic from the business code?

3. Implement controller to decouple bindings from logic code

Careful friends may have noticed that although MVC is mentioned, only Model class appears in the above text, and it is understandable that View class does not appear. After all, HTML is the ready-made View (in fact, from the beginning to the end in this paper, we only use HTML as View, and View class does not appear in the code of javascript). So why is THE Controller class invisible? Don't worry, the so-called "logic code" is a piece of code that has a high coupling between the framework logic (let's call this article's prototype toy a framework) and the business logic, so let's break it down now.
If you want to give the framework the logic for binding, you need to tell the framework how to do it. Since annotation is more difficult to complete in JS, we can do this in view -- using html's tag attributes is an easy and effective way to do this.


function Controller(callback) {
  var models = {};
  //  Find all the bind Element of an attribute 
  var views = document.querySelectorAll('[bind]');
  //  will views Process as a normal array 
  views = Array.prototype.slice.call(views, 0);
  views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    //  Fetch or create the element to which it is bound model
    models[modelName] = models[modelName] || new Model();
    //  Complete the element and specify model The binding of 
    models[modelName].bind(view);
  });
  //  call controller The concrete logic of will models Incoming, convenient for business processing 
  callback.call(this, models);
}





// html:
<div id="div1" bind="model1"></div>
<div id="div2" bind="model1"></div>
//  Logic code: 
new Controller(function (models) {
  var model1 = models.model1;
  model1.set('this is a div');
});


Is it that simple? It's that simple. The essence of MVC is to complete the business logic in controller and modify model, while the change of model causes the automatic update of view, which is reflected in the above code, and supports multiple view and multiple model. It's not enough for a production project, but hopefully it helped you learn a little bit about MVC.

Uncommented "framework" code:


function Model(value) {
  this._value = typeof value === 'undefined' ? '' : value;
  this._listeners = [];
}
Model.prototype.set = function (value) {
  var self = this;
  self._value = value;
  setTimeout(function () {
    self._listeners.forEach(function (listener) {
      listener.call(self, value);
    });
  });
};
Model.prototype.watch = function (listener) {
  this._listeners.push(listener);
};
Model.prototype.bind = function (node) {
  this.watch(function (value) {
    node.innerHTML = value;
  });
};
function Controller(callback) {
  var models = {};
  var views = Array.prototype.slice.call(document.querySelectorAll('[bind]'), 0);
  views.forEach(function (view) {
    var modelName = view.getAttribute('bind');
    models[modelName] = models[modelName] || new Model();
    models[modelName].bind(view);
  });
  callback.call(this, models);
}

Postscript:

In the process of learning flux and redux, although I have mastered the usage of the tools, I just know the reason without knowing the reason. I don't quite understand the "Flux eschews MVC of a a unidirectional flow" which is directly emphasized in the official document of ReactJS. I always feel that one-way data flow is not in conflict with MVC. It is not clear why in the ReactJS document these two are opposed, there is him and there is no self, there is me and there is no other (eschew, avoid). Finally, I made up my mind to go back to the definition of MVC and study it again. Although I was careless in copying and pasting in my daily work, I had to be capricious occasionally, right? This way has helped me to understand this sentence, here can give everyone share their thinking: it MVC and one-way data flow in the flux similar, probably because not distinguish MVC and the relationship between the observer pattern -- MVC is based on the observer pattern, flux too, so the origin of this similarity is the observer pattern, rather than MVC and flux itself. This understanding is also confirmed in the original design pattern of the group of 4: "The first and perhaps best-known example of the Observer pattern appears in Smalltalk Model/View/Controller (MVC), the user interface framework in the Smalltalk environment [KP88]. MVC's Model class plays the role of Subject, while View the base class for observers ".

If the reader is interested in building on this prototype toy, there are 1 directions to follow:

1. Realize the bidirectional binding of input class label 2. Realize the accurate control of scope controlled by controller. Here, one controller controls the entire dom tree 3. Implement the logic of view dom node hide/show, create/destroy 4. Integrated virtual dom to increase the functions of dom diff to improve the rendering efficiency 5. Provide dependency injection to achieve inversion of control 6. Security check the assigned content of innerHTML to prevent malicious injection 7. Implement the logic of model collection, where each model has only one value 8. Use setter in es5 to change the implementation of set method, making it easier to modify model 9. Add control over attributes and css to the view layer 10. Support a syntax similar to double curly braces in AngularJS, with only part of html bound ...

A perfect framework needs to go through countless refinements and modifications. This is just the first step, and there is still a long way to go. I hope you can make persistent efforts.


Related articles: