If you are not particularly clear about the formation of MVVM, read the following section first.

  • Learn about MV* evolution history and features (skip)
  • Understand the observer mode (skip)

What can this article help you understand?

  • Understand how Vue works
  • Referring to the operation mechanism of Vue, change the observer mode to the intermediary mode to achieve a simple MVVM
    • MVVM implementation demonstration
    • MVVM process design
    • Implementation of the mediator pattern
    • Implementation of data hijacking
    • The realization of data bidirectional binding
    • Simple view instruction compiler process implementation
    • The realization of the ViewModel
    • The realization of the MVVM

MV* Evolution history of design patterns

Let’s take a moment to think about how often you hear the word “MVVM” if you’re a developer of a front-end framework (Vue, React, or Angular), but do you really understand what it means?

MV* Origins of design patterns

When computer scientists (and now we are small potatoes) designed GUI (graphical user interface) applications, the code was haphazard and often difficult to manage and maintain. GUI design structure generally includes View, Model, Logic (Application Logic, Business Logic and Sync Logic), such as:

  • The user’s keyboard, mouse and other actions on the View execute Application Logic, which triggers Business Logic to change the Model.
  • After the Model changes, Sync Logic needs to feed back the changes to the View for users to perceive

You can see that views and models are naturally layered in a GUI, and the clutter is mainly logical. As a result, our programmers were constantly racking their brains to optimize the logic of GUI design, and design patterns such as MVC, MVP, and MVVM emerged.

MV* Design patterns in B/S architecture thinking

In B/S architecture application development, the MV* design pattern Outlines and encapsulates the concerns of the application and its environment. Although JavaScript has become a homogeneous language, these concerns may differ between browsers and servers:

  • Can views be used across cases or scenarios?
  • Where should the business logic be handled? (In Model or Controller)
  • How should application state be persisted and accessed?

The MVC (Model View Controller)

Back in the 1970s, Engineers at Xerox developed the Smalltalk programming language and began writing GUIs in it. In smalltalk-80, an engineer named Trygve Reenskaug designed the MVC architecture pattern, which greatly reduced the difficulty of GUI administration.

As shown in the figure, MVC divides GUI into View (View), Model (Model), Controller (Controller) (hot-swappable, mainly for cooperation between Model and View, including routing, input preprocessing and other business logic) three modules:

  • View: Detects the user’s keyboard, mouse and other behaviors, and calls Controller to execute application logic. The View update requires refetching the Model’s data.
  • Controller: Application logic or business logic processing for collaboration between View and Model.
  • Model: Notifies the View through observer mode to update the View when the Model changes.

Updates to the Model through the observer mode, multiple views can share the same Model.

The traditional MVC design is a very beneficial pattern for Web front-end development because views are persistent and can correspond to different models. Backbene. js is a slightly variant implementation of THE MVC pattern (the big difference with the classic MVC is that the View can directly operate on the Model, so the pattern cannot be isomorphic). Here’s a summary of the possible benefits and drawbacks of the MVC design pattern:

Advantages:

  • Separation of responsibilities: high degree of modularization, replaceable Controller, reusability, scalability.
  • Multiple View updates: Using the observer pattern, you can update data by notifying multiple views of a single Model.

Disadvantages:

  • Testing difficulties: The View requires a UI environment, so Controller testing that relies on the View is relatively difficult (many testing frameworks on the Web front end have solved this problem).
  • Strong dependencies: Views are strongly dependent on Model(specific business scenarios), so views cannot be componentized.

The service side MVC

Classic MVC was only used to solve GUI problems, but with the continuous development of B/S architecture, the Web server also derived MVC design pattern.

The evolution of JSP Model1 and JSP Model2

JSP Model1 is an early Java dynamic Web application technology, and its structure is as follows:

In Model1, JSPS contain both Controller and View, while Javabeans contain Controller and Model, and the responsibilities of modules are relatively confusing. On the basis of JSP Model1, Govind Seshadri proposed JSP Model2 pattern by referring to MVC design pattern (see article Understanding JavaServer Pages Model2 Architecture for details). Its structure is as follows:

In JSP Model2, Controller, View and Model division of labor is clear, Model data changes, usually through JavaBean modify View and then front-end real-time rendering, so from the Web front-end request to data echo route is very clear. However, this specifically asks the corresponding backend developer, and may also go from javabeans to Controllers (which primarily identify the JSP corresponding to the current data) to JSPS, so in the server-side MVC, The process View -> Controller -> Model -> Controller -> View is also possible.

In JSP Model2 mode, there is no separation of the front end and the development of the front end is greatly limited.

Model2 derivatives

For Web front-end development, the most intuitive feeling is to derive Model2 patterns from Node services (such as Express and EJS template engines).

The difference between server-side MVC and classic MVC

In the MVC pattern design of the server side, HTTP protocol is used for communication (HTTP is simmon stateless protocol), so the View does not maintain state in different requests (state maintenance needs to be stored through cookies). And in classic MVC, Model informs View through observer mode that the link is broken (for example, it is difficult to implement server push). Of course, in classic MVC, the Controller listens to the View and reacts to input, and the logic becomes heavy, whereas in Model2, the Controller focuses on routing and so on, while the Model deals more with business logic.

MVP (Model – View – the Presenter)

In the 1990s, IBM subsidiary Taligent came up with the MVP concept while developing a graphical interface application called CommonPoint in C/C++.

As shown in the figure above, THE MVP is a modification of the MVC pattern, breaking the View’s dependence on the Model and leaving the rest of the dependencies with MVC unchanged.

  • Passive View: Views no longer handle synchronization logic and are called by an interface to Presenter. Because it no longer relies on the Model, it is possible to detach the View from a particular business scenario and make it entirely componentized.
  • Presenter (Supervising Controller) : Compared to the classic MVC Controller, it is more demanding to handle not only application business logic but also synchronization logic (high level complex UI operations).
  • Model: When a Model changes, the Presenter is notified by the observer mode. If a View is updated, the Presenter may call the View’s interface to update the View.

The possible pros and cons of the MVP model are as follows:

  • Presenter is easy to test, View can be componentized design
  • Presenter is thick and difficult to maintain

MVVM (Model – View – ViewModel)

The MVVM mode is an improvement on the MVP mode, which transforms Presenter into ViewModel:

  • The ViewModel: With Binder(Data binding Engine), MVP’s View and Model updates are manually set by Presenter. Binder binds the View and Model in both ways. To achieve automatic update of View or Model.
  • View: Componentable, for example, with various popular UI component frameworks, Binder automatically updates the corresponding Model with View changes.
  • Model: Changes to the Model are monitored by Binder (again in observer mode), and Binder automatically updates the view once the changes are detected.

You can see that MVVM brings a number of benefits on top of MVP, such as:

  • Improved maintainability, solved a lot of manual synchronization issues with MVP, and provided a bi-directional binding mechanism.
  • The synchronization logic is handled by Binder, and the View changes along with the Model. Therefore, the View is correct only when the Model is correct.

Of course, it also brings some additional problems:

  • Creates performance issues that can cause additional performance costs for simple applications.
  • For complex applications, there are many view states, the maintenance cost of view states increases, and the cost of ViewModel construction and maintenance is high.

MVVM is a very good design pattern for front-end development. In the browser, the routing layer can cede control to the appropriate ViewModel, which in turn can update and respond to persistent views, and with a few minor modifications the MVVM pattern works fine on the server side. The reason for this is that there is no dependency between the Model and View at all (by decoupling the View from the Model, it allows the temporary View to coexist with the persistent View), which allows the View to be rendered through the given ViewModel.

The popular frameworks Vue, React, and Angular are implementations of the MVVM design pattern, and can all implement server-side rendering. Need to pay attention to the current Web front-end development and traditional Model2 need template engine rendering method is different, through Node to start the service for page rendering, and through proxy forwarding request back-end data, completely from the backend of the suffering from the sea out, so that can also greatly liberate the productivity of the Web front-end.

Observer mode and publish/subscribe mode

Observer model

The observer pattern uses a Subject target object to maintain a series of observer objects that depend on it, automatically notifying the series of observer objects of any changes in state. When the Subject target object needs to tell observers what is happening, it broadcasts a notification to them.

As shown in the figure above: When one or more observers are interested in the state of the target object, they can attach themselves to the target object to register the state change of the target object they are interested in. When the state of the target object changes, a notification message will be sent to invoke the update method of each observer. If the observer is not interested in the state of the target object, he can also detach himself from it.

Publish/subscribe

Publish/subscribe pattern using an event channel, the channel between subscribers and publishers, this design pattern allows the code to define the application of specific events, these events can pass the custom parameters, the custom parameter contains the subscriber needs information, using the event channel can avoid the dependent relationship between the publisher and subscribers.

I have used Redis publish/subscribe mechanism for a long time during my student life. For details, I can check Zigbee-door/Zigbee-TCP, but I am ashamed that I have not read the source code of this piece well.

The difference between the two

Observer mode: Allows the observer instance object (the subscriber) to execute the appropriate event handlers to register and receive notifications from the target instance object (the publisher) (that is, to register update methods on the observer instance object), creating a dependency between the subscriber and the publisher without event channels. There is no single object that encapsulates the constraint, and the target object and the observer object must cooperate to maintain the constraint. Observer objects publish events of interest to the objects that subscribe to them. Communication can only be one-way.

Publish/subscribe: A single target usually has many observers, and sometimes one target’s observer is another observer’s target. Communication can be two-way. This pattern is unstable and the publisher is not aware of the status of the subscriber.

Description of the operation mechanism of Vue

Here is a brief description of the running mechanism of Vue (it should be noted that vue.js of Runtime + Compiler is analyzed).

Initialization Process

  • Create Vue instance objects
  • initThe process initializes the life cycle, initializes the event center, initializes the rendering, initializes the executionbeforeCreatePeriodic function, initializationdata,props,computed,watcher, performcreatedPeriodic functions, etc.
  • After initialization, call$mountMethod to mount the Vue instance (the core process of mounting includesTemplate compilation,Apply colours to a drawingAs well asupdateThree processes).
  • If not defined on the Vue instancerenderThe method is definedtemplate, then you need to go through the compile phase. Need to willtemplateString compiled intorender function.templateString compilation steps are as follows:
    • parseCanonical parsetemplateStrings form an AST (abstract syntax tree, which is a tree-like representation of the abstract syntax structure of source code)
    • optimizeMark static nodes to skip diff algorithm (DiFF algorithm is compared layer by layer, only nodes of the same level are compared, so the complexity of time is only O(n). If time complexity is not clear, check out my articleZiyi2 /algorithms-javascript/ Progressive Notation)
    • generateConvert AST torender functionstring
  • Compiled intorender functionAfter the call$mountthemountComponentMethod is executed firstbeforeMountThe hook function, and then the core is to instantiate a renderWatcherIs called in its callback function, which is executed when it is initialized and when data changes are detected in the component instanceupdateComponentMethod (this method callrenderMethod to generate a virtual Node and finally callupdateMethod to update the DOM.
  • callrenderMethod will berender functionRender as virtual Nodes (real DOM elements are huge, because browser standards make DOM very complex. If frequent DOM updates are made, certain performance problems will occur. Virtual DOM uses a native JavaScript object to describe a DOM node, so the cost of Virtual DOM is much smaller than that of creating a DOM, and it is also easy to modify attributes, and it can also achieve cross-platform compatibility.renderThe first argument to the method iscreateElement(or ratherhFunction), this is also explained in the official documentation.
  • After the virtual DOM tree is generated, the call is required to convert the virtual DOM tree into a real DOM nodeupdateMethod,updateMethod is called againpacthMethod to convert the virtual DOM to a real DOM node. Note that creating a real DOM is omitted in this diagram (if you don’t have an old virtual Node, you can just go through itcreateElmCreate a real DOM Node), which focuses on the analysis of the existing virtual Node, will passsameVnodeDetermine whether the current Node to be updated is the same as the old Node (such as the one we set up)keyIf the nodes are different, replace the old node with the new node. If the nodes are the same and there are child nodes, the call is requiredpatchVNodeMethod performs the DIff algorithm to update the DOM to improve the performance of DOM operations.

Note that the reactive process of the data is not described in detail during the initialization phase, which is described in the reactive process.

Responsive flow

  • ininitWill make use ofObject.definePropertyMethods (incompatible with IE8) listen for changes in the responsive data of Vue instances to implement data hijacking capabilities (utilizing the accessor properties of JavaScript objects)getandset– ES6 will be used in future Vue3ProxyTo optimize the reactive principle). During the compilation phase of the initialization process, whenrender functionWhen rendered, reactive data related to the view is read from the Vue instancegetterfunctionDepend on the collection(Will the observerWatcherObject to the subscriber of the current closureDepthesubsAt this point, the data hijacking function and the observer mode are implemented in an MVVM modeBinder, followed by the normal rendering and update process.
  • Data hijacking is triggered when data changes or view-induced data changessetterThe function,setterInitialization is notifiedDepend on the collectionIn theDepAnd corresponding to the viewWatcher, telling you that you need to rerender the view,WatherIt will pass againupdateMethod to update the view.

You can see that as long as you add listening events to the view and automatically change the corresponding data changes, you can achieve two-way data and view binding.

Simple MVVM implementation based on Vue mechanism

After understanding MV* design pattern, observer pattern and Vue operation mechanism, you may have a perceptual cognition of the entire MVVM pattern, so you can implement it manually. The implementation process includes the following steps:

  • MVVM implementation demonstration
  • MVVM process design
  • Implementation of the mediator pattern
  • Implementation of data hijacking
  • The realization of data bidirectional binding
  • Simple view instruction compiler process implementation
  • The realization of the ViewModel
  • The realization of the MVVM

MVVM implementation demonstration

The use of the MVVM example is shown below, These include browser.js(update of View), Mediator.js (mediator), binder.js(data binding engine of MVVM), view.js(View), hijack.js(data hijacking), and mvVM.js (MVVM instance). The code for this example can be found on Github’s Ziyi2 / MVVM:

<div id="app"> <input type="text" b-value="input.message" b-on-input="handlerInput"> <div>{{ input.message }}</div> <div  b-text="text"></div> <div>{{ text }}</div> <div b-html="htmlMessage"></div> </div> <script src="./browser.js"></script>  <script src="./mediator.js"></script> <script src="./binder.js"></script> <script src="./view.js"></script> <script src="./hijack.js"></script> <script src="./mvvm.js"></script> <script> let vm = new Mvvm({ el: '#app', data: { input: { message: 'Hello Input! }, text: 'ziyi2', htmlMessage: '<button> submit </button>'}, methods: { handlerInput(e) { this.text = e.target.value } } }) </script>Copy the code

MVVM process design

Here is a brief description of the operation mechanism of MVVM implementation.

Initialization Process

  • Creates an MVVM instance object and initializes the instance objectoptionsparameter
  • proxyDataThe MVVM instance objectdataThe data is brokered to the MVVM instance object
  • HijackClass to implement data hijacking (to listen for responsive data corresponding to the MVVM instance and view, which is different from VuegetterRely on the collection function)
  • Parse view instructions, convert DOM elements associated with MVVM instances and views into document fragments and parse binding instructions (b-value,b-on-input,b-htmlAnd so on, is actually a super simplified version of Vue compilation),
  • Add data subscriptions and user listening events to mount data corresponding to view instructions to Binder data binding engine (notify Binder Binder to update view via Pub/Sub mode when data changes)
  • Use Pub/Sub mode instead of Observer mode in Vue
  • BinderThe command mode is used to parse view instructions, callupdateMethod to update the View of the document fragment after View parsing binding instructions
  • BrowserThe appearance mode is used for simple compatibility processing of browser

Responsive flow

  • Listens for user input events
  • Call the data set method of the MVVM instance object to update the data
  • Data hijacking triggersettermethods
  • Publish data changes through a publish mechanism
  • The subscriber receives notification of data changes and updates the view corresponding to the data

Implementation of the mediator pattern

The simplest mediator pattern only needs to implement publish, subscribe, and unsubscribe. Information is passed between publication and subscription through event channels to avoid dependencies in the observer pattern. The code for the mediator pattern is as follows:

class Mediator {
  constructor() {
    this.channels = {}
    this.uid = 0
  }

  /** * @desc: subscribe channel * @parm: {String} channel * {Function} cb callback */  
  sub(channel, cb) {
    let { channels } = this
    if(! channels[channel]) channels[channel] = []this.uid ++ 
    channels[channel].push({
      context: this.uid: this.uid,
      cb
    })
    console.info('[mediator][sub] -> this.channels: '.this.channels)
    return this.uid
  }

  /** * @desc: release channel * @parm: {String} channel * {Any} data data */  
  pub(channel, data) {
    console.info('[mediator][pub] -> chanel: ', channel)
    let ch = this.channels[channel]
    if(! ch)return false
    let len = ch.length
    // Last subscription is triggered first
    while(len --) {
      ch[len].cb.call(ch[len].context, data)
    }
    return this
  }

  /** * @desc: unsubscribe * @parm: {String} uid Subscribe identifier */  
  cancel(uid) {
    let { channels } = this
    for(let channel of Object.keys(channels)) {
      let ch = channels[channel]
      if(ch.length === 1 && ch[0].uid === uid) {
        delete channels[channel]
        console.info('[mediator][cancel][delete] -> chanel: ', channel)
        console.info('[mediator][cancel] -> chanels: ', channels)
        return
      }
      for(let i=0,len=ch.length; i<len; i++) {
          if(ch[i].uid === uid) {
            ch.splice(i,1)
            console.info('[mediator][cancel][splice] -> chanel: ', channel)
            console.info('[mediator][cancel] -> chanels: ', channels)
            return
          }
      }
    }
  }
}
Copy the code

In each MVVM instance, a mediator instance object needs to be instantiated. The mediator instance object can be used as follows:

let mediator = new Mediator()
/ / subscribe channel1
let channel1First = mediator.sub('channel1', (data) => {
  console.info('[mediator][channel1First][callback] -> data', data)
})
Subscribe again to Channel1
let channel1Second = mediator.sub('channel1', (data) => {
  console.info('[mediator][channel1Second][callback] -> data', data)
})
/ / subscribe channel2
let channel2 = mediator.sub('channel2', (data) => {
  console.info('[mediator][channel2][callback] -> data', data)
})
// Publish (broadcast)channel1, at which point the subscription to channel1's two callback functions are executed consecutively
mediator.pub('channel1', { name: 'ziyi1' })
// Publish (broadcast)channel2, at which point the subscribe channel2 callback executes
mediator.pub('channel2', { name: 'ziyi2' })
// Unsubscribe channel1 identified as channel1Second
mediator.cancel(channel1Second)
// Only the channel1First callback from channel1 will be executed
mediator.pub('channel1', { name: 'ziyi1' })
Copy the code

Implementation of data hijacking

Object properties

The properties of the object can be divided into data properties ([[Value]], [[Writable]], [[Enumerable]], [[64x]]) and memory/accessor properties (features include [[Get]], [[Set] ], [[Enumerable]], [[Different]]), the property of the object can only be one of the data properties or accessor properties. The different properties of the object are different, different and different.

  • [[Configurable]]: indicates whether the application can passdeleteRedefining a property by deleting it, changing its properties, or changing it to an accessor property.
  • [[Enumerable]]: Enumerability of object properties.
  • [[Value]]: Property value, read from this position when reading property value; When writing property values, store the new values in this location. The default value for this feature isundefined.
  • [[Writable]]: Indicates whether the value of an attribute can be modified.
  • [[ Get ]]: The function that is called when a property is read. The default value isundefined.
  • [[ Set ]]: The function that is called when an attribute is written. The default value isundefined.

Data hijacking is the use of [[Get]] and [[Set]] features, when accessing the property of the object and writing the property of the object can automatically trigger the property of the call function, so as to achieve the purpose of monitoring data changes.

Object attributes can be changed using ES5’s attribute setting method Object.defineProperty(data, key, Descriptor), where descriptor is passed in the property set described above.

The data was hijacked

let hijack = (data) = > {
  if(typeofdata ! = ='object') return
  for(let key of Object.keys(data)) {
    let val = data[key]
    Object.defineProperty(data, key, {
      enumerable: true.configurable: false,
      get() {
        console.info('[hijack][get] -> val: ', val)
        // What is the difference between this and return data[key]?
        return val
      },
      set(newVal) {
        if(newVal === val) return
        console.info('[hijack][set] -> newVal: ', newVal)
        val = newVal
        // If the new value is object, its properties are hijacked
        hijack(newVal)
      }
    })
  }
}

let person = { name: 'ziyi2'.age: 1 }
hijack(person)
// [hijack][get] -> val: ziyi2
person.name
// [hijack][get] -> val: 1
person.age
// [hijack][set] -> newVal: ziyi
person.name = 'ziyi'

// Attribute type change hijacking
// [hijack][get] -> val: { familyName:"ziyi2", givenName:"xiankang" }
person.name = { familyName: 'zhu'.givenName: 'xiankang' }
// [hijack][get] -> val: ziyi2
person.name.familyName = 'ziyi2'

// Data attributes
let job = { type: 'javascript' }
console.info(Object.getOwnPropertyDescriptor(job, "type"))
Accessor properties
console.info(Object.getOwnPropertyDescriptor(person, "name"))
Copy the code

Note that Vue3.0 will not use Object.defineProperty for data listening because

  • Cannot listen for array changes (current array listening is based on some method of native arrayshack, so if you want to make arrays responsive, take care to use some of the array methods Vue officially recommends.)
  • Unable to listen for object properties in depth

Vue3.0 will produce Proxy to solve the above pain points, of course, there will be browser compatibility problems (such as the evil Internet Explorer, Can I use Proxy for details).

It should be noted that only one layer of attribute traversal is performed in hijack. If you want to monitor the deep attributes of the object, you need to continue hijack operation on data[key], so as to achieve the deep traversal monitoring of attributes. For details, see MVVM/MVVM /hijack.js.

The realization of data bidirectional binding

As shown in the figure above, two-way data binding mainly includes data changes causing View changes (Model -> monitor data changes -> View), and View changes changing data (View -> user input monitor events -> Model), so as to achieve a strong connection between data and View.

With the addition of user input events and view updates, it is easy to bind data in both directions (a simple Binder, with a lot of code coupling) :

<input id="input" type="text">
<div id="div"></div>
Copy the code
// Listen for data changes
function hijack(data) {
  if(typeofdata ! = ='object') return
  for(let key of Object.keys(data)) {
    let val = data[key]
    Object.defineProperty(data, key, {
      enumerable: true.configurable: false,
      get() {
        console.log('[hijack][get] -> val: ', val)
        // What is the difference between this and return data[key]?
        return val
      },
      set(newVal) {
        if(newVal === val) return
        console.log('[hijack][set] -> newVal: ', newVal)
        val = newVal
        
        // Update all views associated with data.input data
        input.value = newVal
        div.innerHTML = newVal

        // If the new value is object, its properties are hijacked
        hijack(newVal)
      }
    })
  }
}

let input = document.getElementById('input')
let div = document.getElementById('div')

// model
let data = { input: ' ' }

// Data hijacking
hijack(data)

// model -> view
data.input = '11111112221'

// view -> model
input.oninput = function(e) {
  // model -> view
  data.input = e.target.value
}
Copy the code

Data bidirectional binding demo source.

Simple view instruction compiler process implementation

In the MVVM implementation demo, you can see the use of b-value, B-text, B-on-input, b-HTML and other binding attributes (these attributes are self-defined in the MVVM example and are not native attributes of HTML tags, Similar to VUE’s V-HTML, V-model, V-text instructions, etc.), these instructions are only created to facilitate users to perform synchronous binding operations of model and View, and MVVM instance objects are required to recognize these instructions and re-render DOM elements ultimately required, such as

<div id="app">
  <input type="text" b-value="message">
</div>
Copy the code

Eventually, you need to convert it into a real DOM

<div id="app">
  <input type="text" value='Hello World' />
</div>
Copy the code

Then the steps to realize the above instruction parsing are as follows:

  • Get the corresponding#appThe element
  • Convert to a document fragment (removed from the DOM#appAll the children of
  • Recognize the binding directive in the document fragment and remodify the DOM element corresponding to that directive
  • Re-render after processing the document fragments#appThe element

The HTML code is as follows:

<div id="app">
 <input type="text" b-value="message" />
 <input type="text" b-value="message" />
 <input type="text" b-value="message" />
</div>

<script src="./browser.js"></script>
<script src="./binder.js"></script>
<script src="./view.js"></script>
Copy the code

Let’s start with the use of examples

/ / model
let model = {
  message: 'Hello World',
  
  getData(key) {
    let val = this
    let keys = key.split('. ')
    for(let i=0, len=keys.length; i<len; i++) {
      val = val[keys[i]]
      if(! val && i ! == len -1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`)}}return val
  }
}

// Abstract view (implement the function to convert the corresponding model.message in b-value to the final value="Hello World")
new View('#app', model)
Copy the code

In view.js, conversion of elements under #app to document fragments and property traversal of all child elements (for binding property resolution with binder.js)

class View {
  constructor(el, model) {
    this.model = model
    // Get the node to be processed
    this.el = el.nodeType === Node.ELEMENT_NODE ? el : document.querySelector(el)
    if(!this.el) return
    // Convert all child elements of an existing EL element to a document fragment
    this.fragment = this.node2Fragment(this.el)
    // Parse and process binding instructions and modify document fragments
    this.parseFragment(this.fragment)
    // Add the document fragment back to the DOM tree
    this.el.appendChild(this.fragment)
  }

  /** * @desc: convert node to document shard * @parm: {Object} node Node */  
  node2Fragment(node) {
    let fragment = document.createDocumentFragment(),
        child;
    while(child = node.firstChild) {
      // When a node is added to a document fragment, it is automatically removed from the DOM
      fragment.appendChild(child)
    }    
    return fragment
  }

  /** * @desc: parse document fragments */ @parm: {Object} fragment document fragments */  
  parseFragment(fragment) {
    // Class arrays are converted to arrays for traversal
    for(let node of [].slice.call(fragment.childNodes)) {
      if(node.nodeType ! == Node.ELEMENT_NODE)continue
      // Bind view instruction parsing
      for(let attr of [].slice.call(node.attributes)) {
        binder.parse(node, attr, this.model)
        // Remove the binding attribute
        node.removeAttribute(attr.name)
      }
      // Walk through the node tree
      if(node.childNodes && node.childNodes.length) this.parseFragment(node)
    }
  }
}
Copy the code

Binder.js handles binding directives, using b-value parsing as an example

(function(window, browser){
  window.binder = {
    /** * @desc: check whether it is a binding attribute * @parm: {String} Attr Node attribute */  
    is(attr) {
      return attr.includes('b-')},/** * @desc: parse binding instruction * @parm: {Object} attr HTML attribute Object * {Object} node node * {Object} model data */  
    parse(node, attr, model) {
	  // Determine if it is a binding directive, otherwise the property is not processed
      if(!this.is(attr.name)) return
      // Get model data
      this.model = model 
      // b-value = 'message', so attr. Value = 'message'
      let bindValue = attr.value,
	      // 'b-value'.substring(2) = value
          bindType = attr.name.substring(2)
      // Bind view directive b-value processing
      // Command mode is used here
      this[bindType](node, bindValue.trim())
    },
    /** * @desc: Value binding processing (b-value) * @parm: {Object} node Node node * {String} Key model attributes */  
    value(node, key) {
      this.update(node, key)
    },
    /** * @desc: Value binding update (b-value) * @parm: {Object} node Node node * {String} key model attributes */  
    update(node, key) {
	  // this.model.getData is used to get the value of the model object's property
	  // Model = {a: {b: 111}}
	  // <input type="text" b-value="a.b" />
	  // this.model.getData('a.b') = 111
	  
	  browser.val(node, this.model.getData(key))
    }
  }
})(window, browser)
Copy the code

In browser.js, the appearance mode is used to encapsulate the browser’s native events and DOM operations, so as to achieve compatible processing of the browser. Here, only DOM operations required by B-value are encapsulated for easy reading

let browser = {
  @parm: {Object} Node Node * {String} val Node value */  
  val(node, val) {
	// Convert b-value to value. Note that the b-value attribute is removed from view.js after parsing
    node.value = val || ' '
    console.info(`[browser][val] -> node: `, node)
    console.info(`[browser][val] -> val: `, val)
  }
}
Copy the code

At this point, the simplified Model -> ViewModel (not implemented data listening)-> View path in the MVVM example is passed, and you can see a demo of the View binding instruction parsing.

The realization of the ViewModel

ViewModel Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder Binder It also realizes automatic synchronization logic from View to Model, and finally realizes bidirectional data binding.

Therefore, as long as on the basis of the analysis of View binding instructions, add the data listening function of Model (data change update View) and the input event listening function of View (listen to the View to update the corresponding Model data, Note that the Model changes and updates the views associated with the Model due to data monitoring. It is also important to note that the process of updating views with data changes requires the use of a publish/subscribe model, so if you are not clear about the process, go back to the architecture of the MVVM.

On the basis of the implementation of the simple view instruction compilation process to modify, the first is the HTML code

<div id="app">
 <input type="text" id="input1" b-value="message">
 <input type="text" id="input2" b-value="message">
 <input type="text" id="input3" b-value="message">
</div>

<! -- New broker -->
<script src="./mediator.js"></script>
<! -- New data hijacking -->
<script src="./hijack.js"></script>
<script src="./view.js"></script>
<script src="./browser.js"></script>
<script src="./binder.js"></script>
Copy the code

Mediator.js will not describe the implementation of mediator.js, view.js and browser.js will not describe the implementation of simple view instructions.

Examples to use:

/ / model
let model = {
  message: 'Hello World',
  setData(key, newVal) {
    let val = this
    let keys = key.split('. ')
    for(let i=0, len=keys.length; i<len; i++) {
      if(i < len - 1) {
        val = val[keys[i]]
      } else {
        val[keys[i]] = newVal
      }
    }
    // console.log('[mvvm][setData] -> val: ', val)
  },
  getData(key) {
    let val = this
    let keys = key.split('. ')
    for(let i=0, len=keys.length; i<len; i++) {
      val = val[keys[i]]
      if(! val && i ! == len -1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`)}}return val
  }
}
// Publish/subscribe objects
let mediator = new Mediator()
// Data hijacking (listen for model changes and publish model data changes)
hijack(model, mediator)
// Abstract view (implements resolution of binding directives and updates views by subscribing to changes in model data)
new View('#app', model, mediator)
// Binder -> view (binder -> view)
model.message = 'Hello Ziyi233333222'
Copy the code

First, take a look at data hijacking. The implementation of data hijacking is augmented by the ability to publish data changes for intermediary objects (which are subscribed to in the Binder** of abstract views).

var hijack = (function() {

  class Hijack {
    /** * @parm: {Object} model data * {Object} method */  
    constructor(model, mediator) {
      this.model = model
      this.mediator = mediator
    }
  
    /** * @desc: model data hijacking * @parm: ** /  
    hijackData() {
      let { model, mediator } = this
      for(let key of Object.keys(model)) {
        let val = model[key]
        Object.defineProperty(model, key, {
          enumerable: true.configurable: false,
          get() {
            return val
          },
          set(newVal) {
            if(newVal === val) return
            val = newVal
            // Publish data hijacking changes
            console.log('[mediator][pub] -> key: ', key)
            // Focus on the channel here, which is different from the implementation here in the final MVVM example
            mediator.pub(key)
          }
        })
      }
    }
  }

  return (model, mediator) = > {
    if(! model ||typeofmodel ! = ='object') return
    new Hijack(model, mediator).hijackData()
  }
})()
Copy the code

We then focus on the implementation in binder.js

(function(window, browser){
  window.binder = {
    /** * @desc: check whether it is a binding attribute * @parm: {String} Attr Node attribute */  
    is(attr) {
      return attr.includes('b-')},/** * @desc: parse the binding directive * @parm: {Object} attr HTML attribute Object * {Object} node Node * {Object} model Data * {Object} Mediator */  
    parse(node, attr, model, mediator) {
      if(!this.is(attr.name)) return
      this.model = model 
      this.mediator = mediator
      let bindValue = attr.value,
          bindType = attr.name.substring(2)
      // Bind view instruction processing
      this[bindType](node, bindValue.trim())
    },
    
    /** * @desc: Value binding processing (b-value) * @parm: {Object} node Node node * {String} Key model attributes */  
    value(node, key) {
      this.update(node, key)
      // View -> ViewModel -> Model
      // Listen for user input events
      browser.event.add(node, 'input', (e) => {
        / / update the model
        let newVal = browser.event.target(e).value
        // Set the corresponding model data (because hijack(model) is performed)
        // Mediator.pub is set in hijack(model) and mediator.pub is set
        this.model.setData(key, newVal)
      })

	  // Once the model changes, data hijacking will mediate.pub the changed data
      // Subscribe data changes update view (closure)
      this.mediator.sub(key, () => {
        console.log('[mediator][sub] -> key: ', key)
        console.log('[mediator][sub] -> node: ', node)
        this.update(node, key)
      })
    },
    
    /** * @desc: Value binding update (b-value) * @parm: {Object} node Node node * {String} key model attributes */  
    update(node, key) {
      browser.val(node, this.model.getData(key))
    }
  }
})(window, browser)
Copy the code

The final implementation of a viewModel MVVM simple instance, specific view viewModel implementation demo.

The realization of the MVVM

Based on the implementation of the ViewModel:

  • A newb-text,b-html,b-on-*(event listener) instruction parsing
  • More elegant code encapsulation, new MVVM classes for constraint management of scattered instance objects from previous examples (Builder pattern)
  • hijack.jsRealized withModelDeep monitoring of data
  • hijack.jsPublish and subscribe inchannelProcessing takes the value of the directive bound in the HTML attribute (e.gb-value="a.b.c.d", thenchannelis'a.b.c.d'This is an attempt to change Vue’s observer mode to mediator mode. It is only an implementation method. Of course, the observer mode is more relevant, while the mediator mode is more decoupled.
  • browser.jsAdded compatible handling of event listening,b-htmlandb-textAnd so on instructions DOM operation API

Note that there are still some defects in this example. For example, when the property of Model is an object and the object is overridden, Publish and subscribe maintained channels do not remove channels that the old property listens to.

The resources

  • GUI Architectures – Multiple Architectures trace the intellectual history of ideas in UI design
  • Understanding JavaServer Pages Model2 Architecture – Tells the story of JSP Model1 reference MVC advanced JSP Model2
  • Scaling Isomorphic Javascript Code – First screen speed and SEO Optimization problems revelation of single page apps
  • Below interface: Restore true MV* mode – learn about MV* mode
  • DMQ/ MVVM – Analysis of vUE implementation principles, their own implementation of MVVM