Learning MVVM Framework

>

MVVM (Model-View-ViewModel) is a programming framework. Vue.js is focused on the ViewMOdel layout of MVVM pattern. It serves as the bridge connecting View and Model through Two Way Data Binding.

ViewModel is an object that syncs the Model and the view. In Vue.js, each Vue instance is a ViewModel.


var vm = new Vue({})
									

View is the actual DOM that is managed by Vue instances. each Vuew instance is associated with a corresponding DOM element. After View is compiles, the View is reactive to data changes. The way to access View:


vm.$el
									

Model in Vue.js is plain JavaScript objects or data objects. When data object is used as data property inside a Vue instance(a component), it becomes reactive. Once any of the properties in that data object is manipulated, all Vue instances using that data object will be noticied the changes and this is achieved using getters and setters.

Data Binding

The core feature of this MVVM framework is data binding.

To make it easier, the two way data binding in Vue.js is that when data object changes, the DOM (View) using that data property will change; when there is a change in DOM(View), the associated data property wiill be updated.

What do we need to construct a MVVM

Here are the things we may need:

  • A DOJO object
  • An object saving data object
  • A mapping tool that can project our data to HTML
  • A method to update data
  • A watcher to watch the change in data property and notify view
  • A HTML tag that can respond to data change

The way we could detect property change in data object is using Object.defineProperry() in ES5. When there is change in data, it will trigger the set method in this function. By using this function to detect data change, we could then create some functions and update our view.


In Vue.js, there are two ways we can get the data from Vue instance. Obviously, getting data directly from Vue instance is simpler. The way to achieve this is to bind all data properties to our Vue instance using proxy. We can use data proxy to bind our data to out Vue instance


const vm = new Vue({
	el:"#app",
	data:{
		name: 'siyu'
	},
	created(){
		//there are two ways of getting data
		const name = this.name
		const name2 = this.data.name
	}
})
									

To compile our DOM tree, we need to traverse all DOM nodes. To be more efficient, we transform all nodes in out DOM tree to DOM fragments and after compling, we add all fragment back to root el.


In Vue, both Compiler and Observer are subscriber and publisher. And the way they communicate with each other is through watcher.


class MVVMObj{
	constructor(data){
		this.$option = option;
		const data = this._data = this.$option.data

		//call proxyData function, bind each property in data object to Vue instance
		this.proxyData(data)

		//compile template
		const DOM = this._el = this.$option.el
		compile(dom, this)
	}
	proxyData(data){
		const that = this
		for(let key in data){
			let val = data[key]
			Object.defineProperty(that, key, {
				enumerable:true,
				get(){ return that._data[key] }
				set(newVal){
					that._data[key] = newVal
				}
			})
		}
	}
}

//Observer type class: use to watch and detect data change
function Observe(){
	constructor(data){
		this.data = data
		this.init(data)
	}

	/*
	data {
		key1: val1,
		key2:{
			subkey2: val,
			subkeys:{
				...
			}
		}
	}
	*/

	init(data){
		for(let k in data){
			let val = data[k]

			//since data can be a nested object, need to check until the innermost layer, using recursive call
			if(typeof val === 'onject'){
				observe(val)
			}

			Object.defineProperty(data, k, {
				enumerable: true,
				get(){
					return val
				},
				set(newVal){
					if(newVal === val){ return; }
					val = newVal

					//if the new value is also an object not a primitive type, then need to add observer to each inner layer
					if(typeof newVal === 'object'){
						observe(newVal)
					}
				}
			})
		}
	}
}

function observe(data){  //Observe type instance
	return new Observe(data)
}

function Compile(){
	constructor(el,vm){
		this.$vm = vm,
		this.$el = document.querySelector(el)

		//step1: change all DOM nodes to fragment
		this.$fragment = this.nodeToFragment(this.$el)

		//step2: check all labels and bind data
		this.compileElement(this.$fragment);

		//step3: add all fragments back
		this.$el.appendChild(this.$fragment);

	}

	nodeToFragment(el){
		let nodeFragment = document.createDocumentFragment();

		//loop through all nodes in root el and append to nodeFragment
		while(child = el.firstChild){
			nodeFragment.appendChild(child)

		}
		return nodeFragment
	}

	compileElement(node){
		let reg = /\{\{(.*)}\}/  //we are checking {{}}
		Array.from(node.childNodes).forEach((node)=>{
			let test = node.textContent;
			if(node.nodeType === 3 && reg.test(text)){
				let arr = RegExp.$1.split('.')

				let val = vm;
				arr.foreach((k)=>{
					val = val[k]
				})

				node.textContent = text.replace(/\{\{(.*)}\}/ , val)

				if(node.childNodes){
					this.compileElement(node)
				}
			}
		})
	}
} //Compile type class

function compile(el){
	return new Compile(el) //Compile type instance
}

//a class to handle publish (execute function) and subscribe (add in funciton)
//Dep is a repository hosting all watchers
class Dep{
	constructor(){
		this.subs=[]
	}

	//add subscriber
	addSub(sub){
		this.subs.push(sub)
	}

	//notify subscriber
	nofity(){
		this.subs.forEacy((sub)=>{
			sub.update()
		})
	}
}

//Watcher class: all dojo created with Watcher type need to pass in a function when construct
class Watcher{
	construcor(fn){
		this.fn = fn
	}

	update(){
		this.fn()
	}
}
const watcher1 = new Watcher(()=>{
	console.log('my update function')
})

const dep = new Dep()

dep.addSub(watcher1)
dep.notify()
									

Two-Way data binding example:

One typical use case of this reactive two-way data binding is v-model. When the bound value changes, the page will automatically render the corresponding value and update. Only thing we need to do is to bind the correct data to the DOM.


 < template > 
	 < div > 
		 < input type='text' v-model='user' > 
		 < div>{{user}} < /div > 
	 < /div > 
	 < script > 
		export default {
			data: {
				return {
					user: 'myName'
				}
			}
		}
	 < /script > 
 < /template >