1. 程式人生 > 其它 >MVVM實現類似Vue的基本功能

MVVM實現類似Vue的基本功能

html檔案中引入自定義的MVVM.js

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<div id="app">
			<input type="text" v-model="school.name" />
			{{school.name}}
			<div>{{school.name}}</div>
			<div>{{school.age}}</div>
			<div v-html="message"></div>
			
			<ul>
				<li>1</li>
				<li>2</li>
			</ul>
			{{getMyLove}}
			<button v-on:click="change">+</button>
		</div>
		<script src="./自定義mvvm.js"></script>
		<script>
			let vm=new Vue({
				el:"#app",
				data:{
					school:{
						name: "宋楊",
						age:22
					},
					message:"<h1>大家好</h1>"
				},
				computed:{
					getMyLove(){
						return this.school.name+"喜歡琳琳";
					}
				},
				methods:{
					change(){
						this.school.age=this.school.age+1;
						//為什麼這樣寫不行 this.school.age=this.school.age++;
					}
				}
			})
		</script>
	</body>
</html>

我們需要先定義幾個關鍵的類:

Vue:整個框架的入口。
Observer:把$data資料全部轉化成Object.defineProperty()來定義。資料劫持。
Watcher:觀察者,每個使用到資料的地方都要建立觀察者,並且繫結到對應的被觀察者身上,該過程就是訂 閱。
Dep:是一個包含釋出和訂閱的功能,data裡面的每個資料都需要有一個Dep例項。
Compiler:用來編譯模版的,簡單實現了虛擬DOM的功能。

2.簡述一下大致的流程。
首先,進入的是vue例項,在裡面獲取對應的el、data、computed、method等資料。先把data的資料全部轉化成Object.defineProperty()定義。然後把computed、method這些方法新增代理,例如,用this.XXX就能指代this.computed.XXX()。再把資料獲取操作vm.$data上的取值操作 都代理到 vm上。最後就可以建立Compiler例項編譯模版了。
注意:代理操作其實是更方便新手入門,更簡單的操作資料。
Observer類裡面會將data物件裡面的屬性全部用Object.defineProperty()定義,若屬性也是物件,就把該屬性裡面的子屬性也重定義,使用了遞迴的思想。同時,在每一個的get方法裡面都有訂閱觀察者的功能,在每一個set方法裡面都有釋出功能。
Dep就是在Observer類中使用訂閱和釋出功能時候需要呼叫的類。
Compiler模版編譯中,先獲取到對應的根節點,用document。createDocumentFragment()建立文件碎片,將根結點的child都用append移動到文件碎片中。這樣的話就相當於是虛擬Dom,最後使用的時候把該文件碎片塞回到頁面中。
同時分別對元素和文字編譯,獲取到對應的指令或{{}},對v-model和{{}}都要新增觀察者,並且把對應的值傳到頁面中。

MVVM.js

class Compiler {
	constructor(el, vm) {
		// 判斷el是字串還是元素。如果是字串,通過字串獲取相應元素
		this.el = this.isElementNode(el) ? el : document.querySelector(el);

		// 把當前節點中的元素 獲取到 放到記憶體儲中
		this.vm = vm;
		let fragment = this.createFragment(this.el);

		// 把節點中的內容替換
		// 用資料編譯模版
		this.compiler(fragment);

		// 把內容塞到頁面中

		this.el.appendChild(fragment);

	}
	isDirective(attrName) { //判斷是不是指令
		return attrName.startsWith('v-');
	}
	compilerElement(node) { //編譯元素
		let attributes = node.attributes;
		[...attributes].forEach(attr => {
			let {name,value:expr} = attr;
			if (this.isDirective(name)) {
				let [,directive]=name.split('-');
				
				let [directiveName,eventName]=directive.split(':');
				
				//需要呼叫不同的指令來處理
				CompilerUtil[directiveName](node,expr,this.vm,eventName);
			}
		})
	}
	compilerText(node) { //編譯文字,判斷文字中是否有{{}}
		let content = node.textContent;
		if(/\{\{(.+?)\}\}/.test(content)){
			//文字節點
			CompilerUtil['text'](node,content,this.vm);//{{a}} {{b}}
		}
	}
	//核心編譯方法
	compiler(node) { //編譯記憶體中dom
		let childNodes = node.childNodes;

		[...childNodes].forEach(child => {
			if (this.isElementNode(child)) {
				this.compilerElement(child);
				//如果是元素的話 需要把自己傳進去 再去遍歷子節點
				this.compiler(child);

			} else {
				this.compilerText(child);
			}
		})

	}

	createFragment(node) { //把節點移動到記憶體中
		// 建立文件碎片
		let fragment = document.createDocumentFragment();
		let firstChild;
		while (firstChild = node.firstChild) {
			// appendChild()具有移動性
			fragment.appendChild(firstChild);
		}
		return fragment;
	}
	isElementNode(node) { //判斷是否為元素節點
		return node.nodeType === 1;
	}

}
CompilerUtil={
	getValue(vm,expr){
		
			return expr.split('.').reduce((data,current)=>{
				return data[current];
			},vm.$data)
		
	},
	setValue(vm,expr,value){
		expr.split('.').reduce((data,current,index,arr)=>{
			if(index==arr.length-1){
				return data[current]=value;
			}
			return data[current];
		},vm.$data)
	},
	on(node,expr,vm,eventName){
		node.addEventListener(eventName,(e)=>{
			vm[expr].call(vm,e);
		});
	},
	model(node,expr,vm){
		let fn=this.updater['modeUpdater'];
		
		new Watcher(vm,expr,(newValue)=>{//新增觀察者
			fn(node,newValue);
		});
		node.addEventListener('input',(e)=>{
			let value=e.target.value;
			this.setValue(vm,expr,value);
		});
		let value=this.getValue(vm,expr);
		fn(node,value)
	},
	html(node,expr,vm){
		let fn=this.updater['htmlUpdater'];
		
		new Watcher(vm,expr,(newValue)=>{//新增觀察者
			fn(node,newValue);
		});
		let value=this.getValue(vm,expr);
		fn(node,value)
	},
	getContentValue(vm,expr){
		return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
			return this.getValue(vm,args[1]);
		});
	},
	text(node,expr,vm){
		let fn=this.updater['textUpdater'];
		let content=expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
			new Watcher(vm,args[1],()=>{//新增觀察者
				fn(node,this.getContentValue(vm,expr));//返回一個全的字串
			}); 
			return this.getValue(vm,args[1]);
			
		});
		fn(node,content);
		
	},
	updater:{
		modeUpdater(node,value){
			node.value=value;
		},
		htmlUpdater(node,value){
			node.innerHTML=value;
		},
		textUpdater(node,value){
			node.textContent=value;
		}
	}
}


//觀察者 (釋出訂閱) 觀察者(多) 被觀察者(一)
class Dep {
	constructor() {
		this.subs=[];//存放所有的watcher
	}
	//訂閱
	addSub(watcher){//新增watcher方法
		this.subs.push(watcher);
	}
	
	//釋出
	notify(){
		this.subs.forEach(watcher=>watcher.updata());
	}
}

// vm.$watch(vm,'school.name',(newValue)=>{})
class Watcher{
	constructor(vm,expr,cb){
		this.vm=vm;
		this.expr=expr;
		this.cb=cb;
		this.oldValue=this.get();
	}
	get(){
		//取資料之前吧當前的觀察者放到Dep.target裡面
		Dep.target=this;
		//由於getValue()會觸發Object.defineProperty的get方法,在get裡面會將此watcher新增到所屬的Dep例項裡面。
		let value=CompilerUtil.getValue(this.vm,this.expr);
		//取完資料之後必須賦空,否則後面其他地方會無法更新Dep.target的值,意味著後面的訂閱都會出錯。
		Dep.target=null;
		return value;
	}
	updata(){
		let newValue =CompilerUtil.getValue(this.vm,this.expr);
		if(newValue != this.oldValue){
			this.cb(newValue);
		}
	}
}

class Observer{
	constructor(data) {
	    this.observer(data);
	}
	// 實現資料劫持功能
	observer(data){
		//如果是物件才觀察
		if(data && typeof data == 'object'){
			//如果是物件
			for(let key in data){
				this.defineReactive(data,key,data[key]);
			}
		}
	}
	defineReactive(obj,key,value){
		this.observer(value);
		let dep =new Dep();//給每一個屬性都加上具有釋出和訂閱的功能
		Object.defineProperty(obj,key,{
			get(){
				
				Dep.target && dep.addSub(Dep.target);
				return value;
			},
			set:(newValue)=>{
				if(value != newValue){
					// 賦的新值,需要把它再監控下
					this.observer(newValue);
					value=newValue;
					dep.notify();
				}
				
			}
		});
	}
}

class Vue {
	constructor(options) {
		this.$el = options.el;
		this.$data = options.data;
		let computed=options.computed;
		let methods=options.methods;
		
		//根結點存在,編譯模版
		if (this.$el) {
			// 把資料全部轉化成Object。defineProperty()來定義
			new Observer(this.$data);
			
			for(let key in computed){
				Object.defineProperty(this.$data,key,{
					get:()=>{
						return computed[key].call(this)
					}
				});
			}
			
			for(let key in methods){
				Object.defineProperty(this,key,{
					get(){
						return methods[key];
					}
				});
			}
			
			// 把資料獲取操作 vm上的取值操作 都代理到vm.$data
			this.proxyVm(this.$data);
			
			new Compiler(this.$el, this);
		}
	}
	proxyVm(data){
		for(let key in data){
			Object.defineProperty(this,key,{
				get(){
					return data[key]; //進行轉化
				},
				set(newValue){
					data[key]=newValue;
				}
			});
		}
	}

}