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;
				}
			});
		}
	}

}


MVVM实现类似Vue的基本功能

上一篇:001. 配置安全的签名信息


下一篇:protobuf 使用