vue源码手写mvvm简易实现

即了解了底层实现原理和过程,又锻炼编程能力

第一部 实现html模板 模拟vue 传数据new出来实例

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0">
	<title>Document</title>
</head>
<body>
	<div id="app">
		<div class="box">
		年龄{{gpc.age}}----
		姓名 {{gpc.name}}
			 <ul>
	 			<li>郭大爷</li>
	 			<li>{{gpc.name}}</li> 
	 		</ul>
	 		<input type="text" v-model="message">
	 	</div>
	</div>
</body>
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
	let vm = new mvvm({
		el: "#app",
		data: {
			message: "hello gpc",
			gpc:{
				name: "郭大爷",
				age:222
			}
		}
	})
</script>
</html>

第二部 创建构造函数 mvvm.js

class mvvm {
	constructor(options) {
		/**一上来,先把可用的东西挂载到实例上, */
		this.$el = options.el;
		this.$data = options.data;
		if (this.$el) {
			//数据劫持 把所有数据都set get化
			new Observer(this);

			//数据代理 this.$data.msg获取数据 变成 this.msg
			this.proxyData(this.$data);
			//用数据和元素编译
			new Compile(this.$el, this); 
		}
	}
	//代理的函数
	proxyData(data){
		Object.keys(data).forEach(key=>{
			Object.defineProperty(this,key,{
				get(){
					return data[key]
				},
				set(newValue){
					data[key] = newValue;
				}
			})
		})
	}
}

第三部 实现数据劫持 定义Observer.js

		//要将数据--劫持 先获取到data 和 key 和 value
		Object.keys(data).forEach(key=>{
			this.defineReactive(data,key,data[key]);
			//递归继续劫持
			this.observer(data[key]);
		})

	}
	//定义响应式
	defineReactive(obj,key,value){
		let that = this;
		//每个比变化的数据,都会对应一个数组,这个数组存放的是所有更新的操作
		let dep = new Dep();
		Object.defineProperty(obj,key,{
			enumerable:true,
			configurable:true,
			get(){ //取值的时候
				Dep.target && dep.addSub(Dep.target)
				return value;
			},
			set(newValue){ //当给data属性中设置的时候
				if(newValue!=value){
					that.observer(newValue); //如果是对象继续劫持
					value=newValue;
					dep.notify(); //当改变值的时候,通知所有人,数据变化了
				}
			}
		})
	}
}

class Dep{
	constructor(){
		//发布订阅者模式 
		this.subs = [];
	}

	addSub(watcher){
		this.subs.push(watcher)
	}

	notify(){
		this.subs.forEach(watcher=>watcher.update())
	}
}

第四步 实现数据的编译 compile.js

class Compile {
	constructor(el, vm) {
		//编译的方法也写成一个构造函数
		this.el = this.isElementNode(el) ? el : document.querySelector(el);
		this.vm = vm;
		if (this.el) {	/**如果元素存在就开始编译dom*/
		
			//一、先把dom放入内存中,内存中操作dom性能比较友好	
			//二、通过fragment 把dom放入内存中
			let fragment = this.node2fragment(this.el);
			//编译放入内存中的元素节点和文字节点
			this.compile(fragment);
			//三、 编译完了,再放入页面中
			this.el.appendChild(fragment)
		}
	}

	/**专门写一些辅助的方法 */
	isElementNode(node) {
		return node.nodeType === 1;
	}

	/**核心方法 */

	//编译元素
	compileElement(node) {
		let attr = node.attributes;
		Array.from(attr).forEach(attr => {
			if (attr.name.includes('v-')) {
				//带v-model v-html的
				//取到对应的值放入节点中
				let expr = attr.value
				// console.log('编译元素 v-model 类型的')
				let [, type] = attr.name.split('-');
				CompileUtil[type](node,this.vm, expr);
			}
		})
	}

	//编译文本 
	compileText(node) { //node是传进来的
		//正则找到带插值表达式的
		let expr = node.textContent;
		let reg = /\{\{([^{|}]+)\}\}/g;
		if (reg.test(expr)) {
			//带花括号的才编译文本
			//node  this.vm.$data text
			CompileUtil['text'](node,this.vm,expr);
		}
	}

	//总的编译函数
	compile(fragment) {
		let childNodes = fragment.childNodes;
		Array.from(childNodes).forEach(node => {
			//1是元素 2 是属性 3 是文本

			if (this.isElementNode(node)) {
				//编译元素,只是编译带v-的
				this.compileElement(node);
				this.compile(node)
			} else {
				//编译文本只编译{{}}
				this.compileText(node);
			}
		})
	}

	//抽取虚拟dom
	node2fragment(el) { //需要将el中所有内容放入到内存中
		//文档碎片
		let fragment = document.createDocumentFragment();
		let firstChild;
		while (firstChild = el.firstChild) {
			//这个循环条件太优秀了
			fragment.appendChild(firstChild);
		}
		return fragment; //返回内存中的节点
	}
}


//编译替换 单对象
CompileUtil = {
	//获取值
	getVal(vm, expr) {
		let arr = expr.split('.');
		// 下一个、的结果, 溢奶 上一个的结果
		return arr.reduce((prev,item)=>{
			return prev[item]
		},vm.$data)
	},
	//提取文字
	getTextVal(vm,str){
		return str.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
			return this.getVal(vm,arguments[1])
		})
	},

	text(node, vm, expr) { //文本处理
		let updateFn = this.updater['textUpdater'];
		//提取文字分割成数组()
		//如果是多个 分多次扔进去处理
		let value=this.getTextVal(vm,expr);
		//  这个函数只执行一次,但是正则里的new watcher
		//   需要监听2次 a一次 和 b一次
		expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
			new Watcher(vm,arguments[1],(newValue)=>{
				//当a发生变化时,需要获取2次值,a的值和b的值  来更新节点中的值
				//这里为什么不可以用回调函数里面的参数newValue
				//应为newValue时变化的那个值,不能把a的新值给节点,从而丢失b的老值
				 updateFn && updateFn(node,this.getTextVal(vm,expr));
			})
		})

		//如果一个文本节点里有多个插值表达式,需要监听-值{{a}} {{b}} 

		//初始编译模板的时候就加一个监听,getset ,如果数据变化了,调用watch的callback方法的回调函数
	
		//按数组 this.data.gpc.name格式 提取data里面的内容 
		 updateFn && updateFn(node,value);
	},
	setValue(vm,expr,newValue){
		expr = expr.split('.');
		expr.reduce((prev,item,curindex)=>{
			if(curindex===expr.length-1){
				return prev[item] = newValue;
			}
		},vm.$data)
	},
	model(node, vm, expr) {//输入框处理
		//调用Model处理函数
		let updateFn = this.updater['modeUpdater'];
		//编译v-model的时候加上监听
		new Watcher(vm,expr,(newValue)=>{ 
		//这个回调函数在什么时机调用呢,当数据发送改变时
		//数据改变时触发get set
			 updateFn && updateFn(node,this.getVal(vm,expr));
		})

		//添加一个事件
		node.addEventListener('input',e=>{
			let newValue = e.target.value;
			this.setValue(vm,expr,newValue)
		})

 		updateFn && updateFn(node,this.getVal(vm,expr));
	},
	updater: {
		textUpdater(node, value) { //文本更新
			node.textContent = value;
		},
		modeUpdater(node, value) { // 输入框更新
			node.value = value
		}
	}
}

第五部 加入观察这 watcher.js

//观察者的目的 就是给需要变化的那个元素增加一个观察者,
当数据变化后监听 对应的方法
class Watcher{
	constructor(vm,expr,cb){
		this.vm = vm;
		this.expr = expr;
		this.cb = cb;
		//刚初始化就获取值,此时的是老的值
		this.value = this.get()
	}

	getVal(vm,expr){
		expr = expr.split('.');
		return expr.reduce((prev,next)=>{
			return prev[next]
		},vm.$data)
	}
	get(){
		// Dep.target = this; //this就是new watcher的实例
		Dep.target = this;
		// replace(/^\s*|\s*$/g , ""); 匹配空格字符串
		let value = this.getVal(this.vm,this.expr);
		Dep.target = null;
		return value;	
	}
	//对外暴露的方法
	update(){
		//值变化 了再取 就是新值啦
		let newValue = this.getVal(this.vm,this.expr);
		let oldValue = this.value;
		if(newValue!==oldValue){
			this.cb(newValue);// 对应watch的callback方法
		}
	}
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值