A practice for vuejs like MVVM

A practice for vuejs like MVVM

MVVM simple simulation framework implementation A practice for vuejs like MVVM

MVVM simple simulation framework implementation

background

One day when I had nothing to do, I just found a good article about MVVM. After reading it carefully, I felt that I had gained a lot. I decided to record some thoughts and understanding.

summary

Turn the main concepts in this article into my own understanding and record the implementation steps. Not much to say, let's get started.

What is MVVM?

Before implementation, let me briefly mention what it is MVVM. It is a mode of software design architecture. A layer of ViewModel will be established between Model and View to help the interaction between Model and View, which can simplify some tedious repetitions of development. Actions.

What is the relationship between Front End and MVVM?

In Front End development, apart from CSS, the main task is to render data on the screen. In the browser, there are also many methods of this type, collectively referred to as Element Methodsthese methods under the MVC architecture. , Is responsible for handling the interaction between Model and View. JQuery is mainly doing this, but these methods must always be interacted through manual operations. Under the MVVM architecture, these tedious actions are handed over The ViewModel does the work. We only need to process and update the data, and the ViewModel will complete the action of updating the View based on the updated data.

Practical exercises

Instructions before implementation

The way to implement MVVM is not limited to one. This article uses Vuejs 數據劫持訂閱發布method to implement it, mainly focusing on the understanding and practice of MVVM concepts, and the specific functions will not be carefully studied one by one.

First of all, to complete the MVVM architecture, five main modules must be included:

  • Observer: Hijacking settings for data
  • Dep: Subscriber's dependency on watchers used to store hijacked data
  • Watcher: Actions to be performed when storing hijacked data updates
  • Compiler: Compile templates, parse different types of nodes and add watcher
  • MVVM: Integrate all modules and provide users to call
Ideas finishing

Since the final steps to be completed are a bit huge and complex, I will explain in two parts. The first part is the practice of individual modules, and the second part will integrate the modules.

In practice, the code will be executed in the following order:

  1. The user creates an instance and transfers data to the MVVM module
  2. The MVVM module passes the incoming data to Observer and Compiler in order for processing
  3. Observer module sets up a hijacking mechanism for data
  4. Compiler parses the elements, compiles according to the incoming data, and adds the watcher
  5. When Watcher is created in Compiler, it will automatically mount itself to the corresponding data Dependency
  6. Compile is completed, and when the subsequent data is updated, the watchers mounted in all Dependency of the data are called to perform the same actions as when compiling the node.
Practice of Observer module

The first step is to look at how to do Observer hijacking of data, mainly the use of definePropertyproperty to be handed back to the hijacking of all items of property, including sub-attribute with the new values, initial practice as follows:

function  observe ( data )  { 
  if  ( ! data  ||  typeof  data !== 'object' )  return ; 
  Object . keys ( data ) . forEach ( ( key )  =>  defineReactive ( data ,  key ,  data [ key ] ) ) ; 
}

function  DefineReactive ( data ,  key ,  val )  { 
  observe ( val ) ;  // Sub-property recursive 
  Object . defineProperty ( data ,  key ,  { 
    enumerable : true ,  // Traversable 
    configurable : false ,  // No longer define 
    get ( )  { 
      return  val ; 
    } , 
    set ( newValue )  { 
      console .log ( key ,  'Listen to changes' ) ;  // hijack action 
      val  =  newValue ; 
      observe ( newValue ) ;  // recursive new value 
    } 
  } ) ; 
}
Practice of Dep module

The above can preliminarily hijack the data changes, but a single data may be used in multiple places. We need a separate place to store the watchers of each data (when used once, a new watcher must be created for processing), so We need a subscriber to store watchers, the subscriber code is as follows:

function  Dep ( )  { 
  this . subs  =  [ ] ; 
}

Dep . Prototype  =  { 
  // add watcher 
  addSub ( watcher )  { 
    this . Subs . Push ( watcher ) ; 
  } , 
  // notify the execution of all dependent watchers 
  notify ( )  { 
    this . Subs . ForEach ( ( watcher )  =>  watcher . Update ( ) ) ; 
  } 
}

Then, as each material must create a new subscriber to store all watchers of his own, so just in the specific defineReactivemethod used is as follows:

// Slightly above...

function  DefineReactive ( Data ,  Key ,  Val )  { 
  const  DEP  =  new new  Dep ( ) ;  // create its own subscriber belonging to 
  the observe ( Val ) ; 
  Object . Defineproperty ( Data ,  Key ,  { 
    Enumerable : to true , 
    Configurable : to false , 
    / / Omitted... 
    set ( newValue )  { 
      console . Log (key ,  'Listen to changes' ) ; 
      val  =  newValue ; 
      observe ( newValue ) ; 
      dep . notify ( ) ;  // notify all watchers 
    } 
  } ) ; 
}

After completing the notification settings, there is one question left, == How to add watchers to dep? == just mentioned, each material has its own subscription instance, the subscriber is defineReactiveto create a method that is added watcher action must be carried out within the closure, therefore, we will assume that the follow-up through the Dep.targetmount watcher instance attribute, it is added at this time put into the dep.

function  DefineReactive ( Data ,  Key ,  Val )  { 
  const  DEP  =  new new  Dep ( ) ;  // subscriber 
  the observe ( Val ) ; 
  Object . Defineproperty ( Data ,  Key ,  { 
    Enumerable : to true , 
    Configurable : to false , 
    GET ( )  { 
      Dep . target  &&  dep . addSub( Dep . Target ) ;  // add the mounted watcher to the instance 
      return  val ; 
    } , 
    // omit... 
  } ) ; 
}

At this point we must first remember that the Watcher modules will be, we will have to mount watcher instance to Dep.targetthis place after a forced pick up information corresponding to trigger his get to join watcher.

Compiler module practice

Why skip the Watcher module first? Because there are many implementation concepts in the Watcher module, you must first talk about Compiler before you can understand why you want to write it like that, so I think it helps to understand it first to talk about Compiler~

In Compiler, we mainly need to know the object to be compiled and the compiled data, and then make the corresponding compilation method according to the node type. In addition, since the initial compilation will involve a large number of DOM nodes, we will first transfer the elements that need to be compiled It is the form of document fragment processing, and after the final analysis and compilation is completed, it will be inserted back into the original element again. The specific practice is as follows:

function  Compiler ( el ,  data )  { 
  this . $el  =  el ; 
  this . $data  =  data ; 
  if  ( this . $el )  { 
    this . $fragment  =  this . node2Fragment ( this . $el ) ; 
    this . init ( ) ; 
    this . $el . appendChild ( this .$fragment ) ; 
  } 
}

Compiler . Prototype  =  { 
  init ( )  { 
    // The parsed object is the document fragment 
    this . CompileElement ( this . $frament ) ; 
  } , 
  node2Fragment ( el )  { 
    const  fg  =  document . CreateDocumentFragment ( ) ; 
    let  child  =  null ; 
    while  ( child  =  el . firstChild )  { 
      fg .appendChild ( child ) ; 
    } 
    return  fg ; 
  } 
}

Then, to complete the analytical node compileElementmethod, which _getDataValis used to acquire property to write the contents of the path of the object and returns the data path:

Compiler . Prototype  =  { 
  // The above is omitted... 
  // Step 1: Analyze the node type 
  compileElement ( el )  { 
    const  childNodes  =  el . ChildNodes ,  self  =  this ; 
    childNodes . ForEach ( ( node )  =>  { 
      if  ( self . isElementNode ( node ) )  { 
        self . compile ( node ) ; 
      }  else  if ( self . isTextNode ( node ) )  { 
        self . compileText ( node ) ; 
      } 
      // If there are child nodes in the node, go back to this step 
      if  ( self . hasChildNodes ( node ) )  { 
        self . compileElement ( node ) ; 
      } 
    } ) ; 
  } , 
  // Step 2: Compile different types of nodes 
  // 1\. Compile tags: Compare attribute instructions, and call the corresponding instruction function after 
  compile ( node )  { 
    const  attrs  = node . attributes ,  self  =  this ; 
    [ ... attrs ] . forEach ( ( attr )  =>  { 
      const  attrName  =  attr . name ; 
      // Determine whether it is an attribute beginning with v- 
      if  ( self . isDirective ( attrName ) )  { 
        const  the dir  =  attrName . the substring ( 2 ) ;  // instruction name 
        const  exp  = attr . value ;  // instruction content 
        // event attribute 
        if  ( self . isEventDirective ( dir ) )  { 
          self . directives [ 'eventHandler' ] ( node ,  self . _getDataVal ( exp ) ,  dir ,  self . $data ) ; 
        // General 
        }  else  { 
          self . Directives [ dir ] ( node , self . _getDataVal ( exp ) ) ; 
        } 
        // Remove the dedicated attribute 
        node . removeAttribute ( attrName ) ; 
      } 
    } ) ; 
  } , 
  /* - Tool method - */ 
  isElementNode ( node )  { 
    return  node . nodeType  ===  1 ; 
  } , 
  isTextNode ( node )  { 
    return  node . NodeType  ===  3 ; 
  } ,
  hasChildNodes ( node )  { 
    return  node . childNodes  &&  node . childNodes . length ; 
  } , 
  isDirective ( attrName )  { 
    return  attrName . indexOf ( 'v-' )  ==  0 ; 
  } , 
  isEventDirective ( dir )  { 
    return  dir . indexOf ( ' on' )  ===  0 ; 
  } ,
  _getDataVal ( exp )  { 
    let  val  =  this . $data ; 
    exp  =  exp . split ( '.' ) ; 
    exp . forEach ( ( k )  =>  { 
      val  =  val [ k ] ; 
    } ) ; 
    return  val ; 
  } , 
  / * Instruction list*/ 
  directives : { 
    text ( node ,  value ) { 
      node . textContent  =  value ; 
    } , 
    html ( node ,  value )  { 
      node . innerHTML  =  value ; 
    } , 
    show ( node ,  value )  { 
      node . style . display  =  Boolean ( value ) ? null : 'none' ; 
    } , 
    eventHandler ( node ,  value,  dir ,  data )  { 
      const  eventType  =  dir . split ( ':' ) [ 1 ] ; 
      const  fn  =  value ; 
      if  ( eventType  &&  fn )  { 
        node . addEventListener ( eventType ,  fn . bind ( data ) ,  false ) ; 
      } 
    } 
  } 
}

The above only completes the compilation of general label nodes. We still have to complete the compilation of text nodes. Because this part involves the concept of label templates, it must be explained separately. Please forgive me~

Digression: Label template compilation

There are also many practical ways to compile label templates, such as direct comparison, or regular comparison, etc. This time we use the new Function method, using regular comparison and replacing the string with new Function to help us compile all the variables in the string, the render method code is as follows:

function  removeWrapper ( arr )  { 
  let  ret  =  [ ] ; 
  arr . forEach ( ( exp )  =>  { 
    ret . push ( exp . replace ( / [ \{ | \} ] /g ,  '' ) . trim ( ) ) ; 
  } ) ; 
  return  ret ; 
}

function  render ( str ,  data )  { 
  const  self  =  this ; 
  let  exps  =  null ; 
  str  =  String ( str ) ; 
  const  t  =  function ( str )  { 
    const  re  =  / \{ \{ \s * ( [ ^ \} ] + ) ? \s * \} \} /g ; 
    exps =  self . removeWrapper ( str . match ( re ) ) ; 
    str  =  str . replace ( re ,  '" + data.$1 + "' ) ; 
    return  new  Function ( 'data' ,  'return "' +  str  + '"; ' ) ; 
  } ; 
  let  r  =  t ( str ) ; 
  return  { 
    exps , 
    value : r( data ) 
  } ; 
}

Benpian principle is not the focus of the practice, not discussed in depth, this simple renderfunction returns compiled string, as well as to use all expsthe reason to get expsbecause the subsequent addition of watchersthe time must be used.

Back to the topic: Compiler module practice

We just added the above render function prototype and continue to complete the compileTextmethod:

Compiler . Prototype  =  { 
  // The above is omitted... 
  // Step 1: Analyze the node type 
  compileElement ( el )  { 
    const  childNodes  =  el . ChildNodes ,  self  =  this ; 
    childNodes . ForEach ( ( node )  =>  { 
      if  ( self . isElementNode ( node ) )  { 
        self . compile ( node ) ; 
      }  else  if ( self . isTextNode ( node ) )  { 
        self . compileText ( node ) ; 
      } 
      // If there are child nodes in the node, go back to this step 
      if  ( self . hasChildNodes ( node ) )  { 
        self . compileElement ( node ) ; 
      } 
    } ) ; 
  } , 
  // Step 2: Compile different types of nodes 
  // 1\. Compile label: compare attribute instructions, and call the corresponding command function 
  // Omit... 
  // 2\. String compilation: compare words All exp 
  compileText ( node ) used in the string { 
    const  text  =  node . textContent , 
          self  =  this , 
          reg  =  / \{ \{ ( . * ) \} \} / ; 
    if  ( reg . test ( text ) )  { 
      const  { exps , value }  =  self . render ( text . trim ( ) ,  self . $data ); 
      self . directives . text ( node ,  value ) ; 
    } 
  } , 
  render ( str ,  data )  { 
    const  self  =  this ; 
    let  exps  =  null ; 
    str  =  String ( str ) ; 
    const  t  =  function ( str )  { 
      const  re  =  / \{ \{ \s *( [ ^ \} ] + ) ? \s * \} \} /g ; 
      exps  =  self . removeWrapper ( str . match ( re ) ) ; 
      str  =  str . replace ( re ,  '" + data.$1 + "' ) ; 
      return  new  Function ( 'data' ,  'return "' +  str  + '";' ) ; 
    } ;
    let  r  =  t ( str ) ; 
    return  { 
      exps , 
      value : r ( data ) 
    } ; 
  } , 
  removeWrapper ( arr )  { 
    let  ret  =  [ ] ; 
    arr . forEach ( ( exp )  =>  { 
      ret . push ( exp . replace ( / [ \{ | \} ]/g ,  '' ) . trim ( ) ) ; 
    } ) ; 
    return  ret ; 
  } , 
  // omit... 
  /* Instruction list*/ 
  directives : { 
    text ( node ,  value )  { 
      node . textContent  =  value ; 
    } , 
    html ( node ,  value )  { 
      node . innerHTML  =  value ; 
    } , 
    show( node ,  value )  { 
      node . style . display  =  Boolean ( value ) ? null : 'none' ; 
    } , 
    eventHandler ( node ,  value ,  dir ,  data )  { 
      const  eventType  =  dir . split ( ':' ) [ 1 ] ; 
      const  fn  =  value ; 
      if  (eventType  &&  fn )  { 
        node . addEventListener ( eventType ,  fn . bind ( data ) ,  false ) ; 
      } 
    } 
  } 
}

We have completed more than basic first compiled, the next time you want to compile each action is completed, have joined a watcher to help update the information, be able to compile local action again after us, so we will compileand compileTextmethod to do modifications as follows:

Compiler . Prototype  =  { 
  // The above is omitted... 
  compile ( node )  { 
    const  attrs  =  node . Attributes ,  self  =  this ; 
    [ ... attrs ] . ForEach ( ( attr )  =>  { 
      const  attrName  =  attr . Name ;

      if  ( self . isDirective ( attrName ) )  { 
        const  dir  =  attrName . substring ( 2 ) ; 
        const  exp  =  attr . value ; 
        // event attributes 
        if  ( self . isEventDirective ( dir ) )  { 
          self . directives [ 'eventHandler' ] ( node ,  self . _getDataVal ( exp) ,  dir ,  self . $vm ) ; 
        // general 
        }  else  { 
          self . directives [ dir ] ( node ,  self . _getDataVal ( exp ) ) ; 
          new  Watcher ( this . $data ,  exp ,  function ( value )  { 
            self . directives [ dir ] ( node ,  value) ; 
          } ) ; 
        } 
        node . removeAttribute ( attrName ) ; 
      } 
    } ) ; 
  } , 
  compileText ( node )  { 
    const  text  =  node . textContent , 
          self  =  this , 
          reg  =  / \{ \{ ( . * ) \} \} / ; 
    if  ( reg . test ( text) )  { 
      const  { exps , value }  =  self . render ( text . trim ( ) ,  self . $data ) ; 
      self . directives . text ( node ,  value ) ; 
      // In the string node, all exps used are Need to add the listener 
      exps in order . forEach ( ( exp )  =>  { 
        new  Watcher ( this . $data , exp ,  function ( )  { 
          const  { value }  =  self . render ( text . trim ( ) ,  self . $data ) ; 
          self . directives . text ( node ,  value ) ; 
        } ) ; 
      } ) ; 
    } 
  } , 
  // next Slightly... 
}

So far, we have completed the basic Compiler, and then continue to talk about the Watcher module~! ! come on! Come on!

Practice of Watcher module

After implementation Compiler, the next most important Watcher plays strung Compiler and bridges Observer communication, but also the soul of the whole MVVM, in front of the very beginning we mentioned at the Dep.targettime of the mount watcher instance, then you must create a watcher instance , Perform a get action on the specified data to force the watcher to be added to the subscriber. The specific implementation is as follows:

function  Watcher ( data ,  exp ,  cb )  { 
  this . $data  =  data ; 
  this . $exp  =  exp ; 
  this . $cb  =  cb ; 
  this . init ( ) ; 
}

Watcher . Prototype  =  { 
  update ( )  { 
    this . Run ( ) ; 
  } , 
  init ( )  { 
    this . _HasInit  =  false ; 
    this . Value  =  this . Get ( ) ;  // automatically call 
    this at the same time of initialization . _HasInit  =  true ; 
  } , 
  run ( )  { 
    const  value  =  this .get ( ) ; 
    const  oldValue  =  this . value ; 
    if  ( value !== oldValue )  { 
      this . value  =  value ; 
      this . $cb . call ( this . $data ,  value ,  oldValue ) ;  // call the bound compile Action 
    } 
  } , 
  get ( )  { 
    ! This . _HasInit  &&  (Dep . Target  =  this ) ;  // Mount the watcher (can only be mounted during initialization to avoid repeated mounting) 
    const  value  =  this . _GetDataVal ( this . $exp ) ;  // Force a call to the target’s get to trigger the hijacking action 
    Dep . Target  =  null ;  // After the addition is complete, the temporary watcher must be removed 
    return  value ; 
  } , 
  /* Tool method*/ 
  _getDataVal ( exp )  { 
    let  val  =  this . $data ; 
    exp  =  exp. split ( '.' ) ; 
    exp . forEach ( ( k )  =>  { 
      val  =  val [ k ] ; 
    } ) ; 
    return  val ; 
  } 
}

Congratulations! ! Seeing that you have mastered the main core modules of the entire architecture, in the end only the MVVM constructor is left, which is the module called by the user~

Practice of MVVM module (Part 2)

The role of MVVM is to combine all the previous modules, use Observer to hijack data changes, analyze and compile templates through Compiler, and complete the connection between Observer and Compiler through Watcher. The most important thing is to bind all data to the MVVM instance. It is convenient for users to use easily, the specific constructor code is as follows:

function  MVVM ( options )  { 
  this . $options ; 
  this . $data  =  this . $options . data ; 
  this . $computed  =  this . $options . computed ; 
  this . $methods  =  this . $options . methods ;

  // First bind all data to the MVVM instance 
  this . Walk ( this . $data ,  ( key )  =>  this . _ProxyData ( key ) ) ; 
  this . Walk ( this . $computed ,  ( key )  =>  this . _proxyComputed ( key ) ) ; 
  this . walk ( this . $methods ,  ( key )  => this . _proxyMethods ( key ) ) ;

  // Initialize again, because the Compiler must use all computed and methods 
  this . $el  =  this . $options . El  ||  document . Body ; 
  this . Init ( ) ; 
}

MVVM . Prototype  =  { 
  init ( )  { 
    new  Observer ( this . $data ) ; 
    new  Compiler ( this . $el ,  this ) ; 
  } , 
  walk ( data ,  fn )  { 
    return  Object . Keys ( data ) . ForEach ( fn ) ; 
  } , 
  /* 
  Proxys */ _proxyData (Key )  { 
    const  Self  =  the this ; 
    Object . Defineproperty ( Self ,  Key ,  { 
      Enumerable : to true , 
      Configurable : to false , 
      GET ( )  { 
        return  Self . $ Data [ Key ] ; 
      } , 
      SET ( nV )  { 
        Self . $ Data [ key ]  =  nV ; 
      }
    } ) ; 
  } , 
  _proxyComputed ( key )  { 
    const  self  =  this ; 
    const  computed  =  this . $computed ; 
    if  ( typeof  computed  ===  'object' )  { 
      Object . defineProperty ( self ,  key ,  { 
        get : typeof  computed [ key ]  ===  'function'  
                ? computed [key ] 
                : computed [ key ] . get , 
        set : typeof  computed [ key ] !== 'function' 
                ? computed [ key ] . set 
                : function ( )  { } 
      } ) ; 
    } 
  } , 
  _proxyMethods ( key )  { 
    const  self  =  this ; 
    const  methods =  this . $methods ; 
    if  ( typeof  methods  ===  'object' )  { 
      Object . defineProperty ( self ,  key ,  { 
        get : typeof  methods [ key ]  ===  'function'  
                ? ( )  =>  methods [ key ]  
                : function ( )  { } , 
        set : function ( ) { } 
      } ) ; 
    } 
  } 
}

At this point, a simple MVVM architecture is complete. In fact, it is easy to compile templates and add events. The usage example is as follows:

< div  id =" app " >
  Hello World
  < h3 > Title </ h3 > 
  < p > {{ info }} </ p >

  < div > 
    < button  v-on:click =" toggleName " > Toggle </ button > 
    < p  v-show =" showName " > {{ name }} </ p > 
  </ div > 
</ div >
const  vm  =  new  MVVM ( { 
  el : '#app' , 
  data : { 
    name : 'Johnny' , 
    age : 100 , 
    showName : true 
  } , 
  computed : { 
    info ( )  { 
      return  this . name  +  ''  +  this . age ; 
    } 
  } , 
  methods : { 
    toggleName ( ) { 
      this . showName  = ! this . showName ; 
    } 
  } 
} ) ;

The above is the implementation of the basic MVVM conceptual process. Of course, there are many places in the article that can definitely improve the writing or the parts that are not comprehensive. Please forgive me from all experts. This article only exists as a way to deepen your understanding of MVVM and take notes. Thank you again. Reading~

references:

2020-03 Added Component and prop functions

Download Details:

Author: johnnywang1994

Source Code: https://github.com/johnnywang1994/MVVM

vuejs vue javascript

Bootstrap 5 Complete Course with Examples

Bootstrap 5 Tutorial - Bootstrap 5 Crash Course for Beginners

Nest.JS Tutorial for Beginners

Hello Vue 3: A First Look at Vue 3 and the Composition API

Building a simple Applications with Vue 3

Deno Crash Course: Explore Deno and Create a full REST API with Deno

How to Build a Real-time Chat App with Deno and WebSockets

Convert HTML to Markdown Online

HTML entity encoder decoder Online

8 Popular Websites That Use The Vue.JS Framework

In this article, we are going to list out the most popular websites using Vue JS as their frontend framework. Vue JS is one of those elite progressive JavaScript frameworks that has huge demand in the web development industry. Many popular websites are developed using Vue in their frontend development because of its imperative features.

Vue Native is a framework to build cross platform native mobile apps using JavaScript

Vue Native is a framework to build cross platform native mobile apps using JavaScript. It is a wrapper around the APIs of React Native. So, with Vue Native, you can do everything that you can do with React Native. With Vue Native, you get

How to Make a Simple Vue Custom Select Component

In this article, you’ll learn how to build a Vue custom select component that can be easily be styled using your own CSS. In fact, it’s the same component that we use in production on Qvault, and you can see it in action on the playground.

Creating a Custom Tooltip Component in Vue

There are plenty of libraries out there that will have you up and running with a good tooltip solution in minutes. However, if you are like me, you are sick and tired of giant dependency trees that have the distinct possibility of breaking at any time.

Vue ShortKey plugin for Vue.js

Vue-ShortKey - The ultimate shortcut plugin to improve the UX .Vue-ShortKey - plugin for VueJS 2.x accepts shortcuts globaly and in a single listener.