Ice ice 2018-5-1

  • The MVVM introduction
  • Why MVVM
  • Advantages of MVVM emergence
  • Focus on vue.js details

The MVVM introduction

MVVM is the abbreviation of Model-view-ViewModel. It is an architectural mode based on front-end development. Its core is to provide two-way data binding between View and ViewModel, which makes the state change of ViewModel automatically transmitted to View. This is called two-way data binding.

Vue.js is a Javascript library that provides mVVM-style bi-directional data binding, focusing on the View layer. At its heart is the VM in MVVM, which is the ViewModel. The ViewModel is responsible for connecting the View and Model, ensuring consistency between View and data. This lightweight architecture makes front-end development more efficient and convenient.

Why MVVM

MVVM was in 2015, and 2015 was the hottest year for MVVM, and before that, MVC was about 5 years ago, MVC stands for Model-View-Controller, Model-view-controller, which means that a standard Web application consists of three parts:

View is used to present data to the user in a certain way. A Model is just data. The Controller receives and processes requests from the user and returns the Model to the user.

Advantages of MVVM emergence

MVVM is the abbreviation of Model-view-ViewModel. It is an architectural mode based on front-end development. Its core is to provide two-way data binding between View and ViewModel, which makes the state change of ViewModel automatically transmitted to View. This is called two-way data binding.

Vue.js is a Javascript library that provides mVVM-style bi-directional data binding, focusing on the View layer. At its heart is the VM in MVVM, which is the ViewModel. The ViewModel is responsible for connecting the View and Model, ensuring consistency between View and data. This lightweight architecture makes front-end development more efficient and convenient.

Focus on vue.js details

Vue. Js can be said to be the best practice of MVVM architecture, focusing on the ViewModel in MVVM. It not only achieves two-way data binding, but also is a relatively lightweight JS library with simple API and easy to use.

Here’s a quick look at some of the implementation details of vue.js on bidirectional data binding:

Vue implements data binding using getters and setters for Object.defineProperty in conjunction with the observer pattern. When passing an ordinary javascript Object to a Vue instance as its data option, Vue iterates through its properties, turning them into getters/setters with Object.defineProperty. Getters/setters are invisible to the user, but internally they let Vue track dependencies. Notify properties of changes when they are accessed and modified.

//Observer: complie: compile/parseCopy the code
  • Obiect.defineproperty (obiect.defineProperty); obiect.defineProperty (obiect.defineProperty); obiect.defineProperty (obiect.defineProperty)
  • The Complie directive parser, which scans and parses the directives of each element node, replaces data according to the directive template, and binds the Observer to Complie, subscribes to receive notification of each attribute change, and executes the corresponding callback function of the directive binding
  • The Watcher subscriber, acting as a bridge between Observer and Complie, can subscribe to and receive notification of each property change, executing the corresponding callback function of the directive binding.
  • The Dep message subscriber maintains an array internally to collect subscribers (Watcher). Data changes trigger the notify function, which calls the update method of the subscriber.

As you can see, when new Vue() is executed, the Vue enters the initialization stage. On the one hand, the Vue iterates over the attributes in the data option, and Object.defineProperty converts them into getters/setters to implement the data change listening function. Compile, the instruction compiler of Vue, scans and parses the instructions of the element node, initializes the view, and subscribes to Wacther to update the view. Wather adds itself to the message subscriber (Dep) and is initialized.

When data changes, setter methods in the Observer are fired, and the setter immediately invokes the Dep. Notify (), the Dep starts iterating through all the subscribers and calls the subscriber’s update method, which updates the view accordingly when the subscriber is notified

Time to start practicing

Now MVVM bidirectional binding should not be unfamiliar, do not understand, nothing a word against the code, a simple text implementation effect, and Vue the same syntax.

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>

<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World! '
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody! '; }}}); </script>Copy the code

Effect:

Several common ways to implement bidirectional binding

At present, several mainstream MVC (VM) frameworks have implemented one-way data binding, and my understanding of two-way data binding is nothing more than adding change(input) events to inputting elements (input, Textare, etc.) on the basis of one-way binding to dynamically modify model and view, without much depth. So there is no need to worry too much about the implementation of one-way or bidirectional binding.

For example, the following data binding is roughly as follows:

Publisher – subscriber mode (backbone.js)

Dirty checking (angular.js)

Data hijacking (vue.js)

Vm. set(‘property’, value) is the most common way to update data. For more information, click on this article

A bit lowL for now, we prefer to update the data with vm.property = value and update the view automatically, so there are two ways

Dirty check: Angular. js uses dirty check to determine whether or not a view is updated to see if the data has changed. The easiest way to do this is to use setInterval() polling to detect data changes. Angular only enters dirty value detection when the specified event is triggered, roughly as follows:

  • DOM events, such as the user entering text, clicking a button, and so on. ( ng-click )
  • XHR response event ($HTTP)
  • Browser Location change event ($Location)
  • The Timer event (interval )
  • performapply()

Data hijacking: Vue. js adopts the mode of data hijacking combined with the publiser-subscriber mode. Through Object.defineProperty(), it hijacks the setter and getter of each attribute, releases messages to subscribers when data changes, and triggers corresponding listening callback

Start analyzing:

We have learned that VUE uses data hijacking combined with the publisher – subscriber mode to do data binding. The core method is to use object.defondeProperty () to realize the hijacking of attributes, so as to monitor data changes. Undoubtedly, this method is one of the most important and basic contents of text. If you’re not familiar with defineProperty(), click further


The object.defineProperty () method directly defines a new property on an Object, or modifies an existing property of an Object, and returns the Object.

grammar

Object.defindProperty(obj,prop.,descriptor)

Parameter obj: The object on which attributes are to be defined. Prop: Name of a property defined or modified. Descriptor: Attribute descriptor to be defined or modified. The object to which the return value is passed

Note: In ES6, because of the special nature of Symbol type, using Symbol type values for Object keys is different from regular definition or modification. Object.defineproperty is one of the ways to define properties where the key is Symbol.

To implement MVVM two-way data binding, the following must be implemented:

  1. Implement a data listener Observer that listens for all attributes of a data object and notifies subscribers of any changes to the latest values
  2. Implement an instruction parser Compile, scan and parse the instruction of each element node, replace the data according to the instruction template, and bind the corresponding update function
  3. Implement a Watcher that acts as a bridge between the Observer and Compile, subscribing to and receiving notification of each property change, and executing the corresponding callback function of the directive binding to update the view
  4. The MVVM entry function integrates all three

To understand the process:

1. Implement the Observer

In this case, we can use obeject.defineProperty () to listen for property changes. In this case, we will need to recursively traverse the Observer data object, including the attributes of the child property object, with setters and getters, so that we can assign a value to this object. I’m going to fire the setter, and I’m going to listen for changes in the data. The relevant implementation code is as follows:

var data={name:'observer'};
observe(data);
data.name='observer-'; // The value changed observer-- >observer-function observe(data){
    if(! data||typeof data! ='object') {return; } // Retrieve all attributes from object.key (data).foreach (function(key){ defineReactive(data,key,data[key]); })};functiondefineReactive(data,key,val){ observe(val); // Listen for object.defineProperty (data,key, {enumerable:true,// Enumerates different signals:false,// can't define get:function(){
            retuen val;
        },
        
        set:function(newVal){
            console.log('Ha ha ha. Listen for value change ', val.'-->',newval); val=newVal; }})}Copy the code

Now that we can listen for each change, we need to notify the subscriber of the change. So we need to implement the message subscriber, which is very simple to maintain an array, which collects the change of the subscriber’s data and triggers notify, and then call the update method of the subscriber.

/ /... omitfunctiondefineReact(data.key,val){ var dep=new Dep(); observe(val); DefineProperty (data,key,{//.. omitset:function(newVal){
        if(val===newVal) return;
        console.log('Hahaha, I'm listening for a change in value',val,'- >',newVal); val=newVal; dep.notify(); // Notify all subscribers}})}function Dep(){
    this.subs=[];
}

Dep.prototype={
    addSub:function(sub){
        this.subs.push(sub);
    },
    notify:function(){
        this.subs.forEach(function(sub){ sub.update(); }}})Copy the code

So the question is, who are the subscribers? How do I add a subscriber to a subscriber? Var dep=new dep (); var dep=new dep (); Is defined inside the dedineReactive method. To add subscribers via deP, you must do it inside the closure, so you can do it in the getter:

//observer.js //... Omit the Object. DefineProperty (data, the key, the {get:function(){Dep = Dep; Dep = Dep; Dep = Dep; Dep = Dep;returnval; } / /... Omit}); //watcher.js Watcher.prototype={ Dep.target=this; this.value=data[key]; // The getter for the property is triggered to add the subscriber dep. target=null; }Copy the code

2. Compile

== Compile the main thing is to parse the template instructions, replace the variables in the template with data, initialize the render page view, bind the update function to the node corresponding to each instruction, add subscribers to listen to the data, receive notification when the data changes, update the view, ==

Because dom nodes are operated for several times during parsing, in order to improve performance and efficiency, the node EL is first converted into document fragment for parsing and compilation. After parsing, the fragment is added back to the original real DOM node

function Compile(el){
    this.$el=this.isElementNode(el)? el:document.querySelector(el);if(this.$el){
        this.$fragment=this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}

Complie.prototype={
    this.complirElement(this.$fragment); }, node2Fragment:function(el) {var hair fragments = document. CreatDocumentFragment (), the child; // Copy the native node to the fragmentwhile(child =el.firstChild) {fragment.appendChild(child); }returnfragment; }}Copy the code

The compileElement method will scan and parse all nodes and their children, call the instruction render function to render data, and call the instruction update function to bind.

Compile.prototype = { // ... Omit compileElement:function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) { var text = node.textContent; var reg = /\{\{(.*)\}\}/; // Expression text // compiles as an element nodeif (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.The $1); } // Iterate over compiled child nodesif(node.childNodes && node.childNodes.length) { me.compileElement(node); }}); }, compile:function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {// Specify that directives are named after v-xxx // such as <span v-text="content"Var attrName = attr. Name; // v-textif (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if(me.isEventDirective(dir)) {// Event directive, such as V-on :click compileutil. eventHandler(node, me).$vm, exp, dir);
                } else{// common command compileUtil[dir] && compileUtil[dir](node, me.$vm, exp); }}}); }}; Var compileUtil = {text:function(node, vm, exp) {
        this.bind(node, vm, exp, 'text'); } / /... omitbind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater']; // Initialize the view for the first time updaterFn && updaterFn(node, vm[exp]); // instantiate the subscriber, which adds the subscriber to the corresponding attribute message subscriber.functionUpdaterFn && updaterFn(node, value, oldValue); }); }}; // update function var updater = {textUpdater:function(node, value) {
        node.textContent = typeof value == 'undefined' ? ' ': value; } / /... Omit};Copy the code

Recursive traversal ensures that each node and its children are parsed and compiled to the text node, including the {{}} expression declaration. The declaration of directives is marked by node attributes with a specific prefix, such as < SPAN v-text=”content” other-attr v-text is an instruction, and other-attr is not an instruction, but a common attribute. Listening for data and binding updates is handled by adding callbacks to compileutil.bind () via new Watcher() to receive notifications of data changes

At this point, a simple Compile is complete, complete code. The next step is to see Watcher in action as a subscriber

Implement Watcher

Watcher subscribers, acting as a bridge between the Observer and Compile, mainly do the following:

  • 1. Add yourself to the attribute subscriber (DEP) during self instantiation
  • 2. It must have an update() method
  • Update () : dep.notice() : update() : dep.notice() : update() : dep.notice()
   }
    },
    get: function() { Dep.target = this; Var value = this.vm[exp]; Dep. Target = null; // Trigger the getter to add itself to the property subscriber. // Set the valuereturnvalue; }}; Object.defineproperty (data, key, {get:function() {Dep = Dep; Dep = Dep; Dep = Dep; Dep = Dep; Dep = Dep; Dep = Dep;returnval; } / /... Omit}); Dep.prototype = { notify:function() {
        this.subs.forEach(function(sub) { sub.update(); // Call the subscriber's update method to notify the change}); }};Copy the code

When instantiating Watcher, the get() method is called, marking the subscriber to the current Watcher instance with dep.target = watcherInstance, forcing the getter method defined by the property to fire. The current Watcher instance is added to the subscriber DEP of the property so that the watcherInstance is notified of updates when the property value changes.

Ok, Watcher has implemented it as well, complete code. Basically vUE data binding related core several modules are also these several, click here, in the SRC directory to find vUE source code.

4. Implement MVVM

MVVM, as the entry of data binding, integrates Observer, Compile and Watcher, uses Observer to monitor its model data changes, and uses Compile to parse and Compile template instructions. Finally, Watcher is used to build a communication bridge between Observer and Compile to achieve data change -> view update; View Interactive Changes (INPUT) -> Bidirectional binding effect of data model changes.

A simple MVVM constructor looks like this:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}
Copy the code

Var vm = new MVVM({data:{name: ‘kindeng’}}); vm._data.name = ‘dmq’; That’s how you change the data.

This is obviously not what we expected at the beginning. We expected the call to look like this:

var vm = new MVVM({data: {name: 'kindeng'}});
vm.name = 'dmq**'; **Copy the code

Therefore, we need to add an attribute proxy method to the MVVM instance, so that the attribute proxy to access vm is the attribute to access vm._data, after the modification code is as follows:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this; XXX -> vm._data. XXX object.keys (data).function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: functionproxySetter(newVal) { me._data[key] = newVal; }}); }};Copy the code

Object.defineproperty () is used to hijack the read and write rights of the attributes of the VM instance, so that the read and write attributes of the VM instance are changed to read and write the attribute values of the VM._data

At this point, all modules and functionality are complete, as promised at the beginning of this article. A simple MVVM module has been implemented, with the ideas and principles largely derived from the simplified vUE source code. Click here to see all the relevant code for this article. Because the content of this article is more practical, there is a lot of code, and it is not appropriate to list a large amount of code, so it is suggested that those who want to know more about this article can read the source code again, so that it will be easier to understand and master.

Note: this article is a summary of the relevant content of some big guy. Have been learning, share with many small white technology partners.