实现简易vue响应式功能-编译器

234 阅读1分钟

从源码层级理解MVVC模式

观看b站视频——vue源码解析后写的笔记

html结构

<body>
	<div id="app">
	<h1>{{my.name}}-{{my.fav}}</h1>
	<p v-html="str"></p>
	<ul>
		<li>1</li>
		<li>2</li>
		<li>3</li>
		<li>{{my.age}}</li>
	</ul>
	<span v-text="my.fav"></span>
	<span v-text="msg"></span>
	<h2>{{msg}}</h2>
	<h3 v-on:click="handleClick">点我打印</h3>
	<input type="text" v-model="msg">
    </div>
</body>
<script src="./MVue.js"></script>//编译器
<script src="./Observer.js"></script>//观察者
<script>
	let vm= new MVue({
		el:'#app',
		data:{
			my:{
				name:'咔酱',
				fav:'爆炸',
				age:'16'
			},
			msg:'西内',
			str:'<h1>爆杀地</h1>'
		},
		methods:{
			handleClick(){
				console.log('consoleCLick')
			}
		}
	})
</script>

整体思路是:

Compile 解析器

1.用document.fragment创建文本流把我们要改动的节点append进去,减少页面回流和重绘

2.获取节点属性,如果节点属性有指令,根据不同指令,调用不同解析方法,解析方法替换值

3.把替换完毕的文档流放入el(id="app"的节点)下。

class MVue{
	constructor(options){//options:vm实例
		this.$el=options.el;
		this.$data=options.data;
		this.$options=options;
		if(this.$el){//如果传了一个根节点
		   //1.实现一个观察者
                  //2.实现一个指令解析器 compile
                  new Compile(this.$el,this); 
		}
	}
}

创建文本流和遍历节点属性拿出其中指令并调用对应update方法

class Compile{
    constructor(el,vm){
		this.el=this.isElementNode(el)?el:document.querySelector(el);
		//如果它是一个元素节点,如果不是就获取		
		this.vm=vm;
		//1.创建一个文档碎片对象,把我们要改动的节点append进去,减少页面回流和重绘
		const fragment=this.node2Fragement(this.el);

		//2.编译节点中我们的指令
		this.compile(fragment);

		//3.把文档碎片放入el下
		this.el.appendChild(fragment);
	}
	//1.创建文本流,并遍历el下子节点加入其中
	node2Fragement(el){
		let firstChild;
		let f=document.createDocumentFragment();
		while(firstChild=el.firstChild){
			f.appendChild(firstChild);
		}
		return f;
	}
	//2.解析文本流,遍历其中节点,分文本节点和元素节点,如果节点中有子节点,递归调用compile
		compile(fragment){
		const childNodes=fragment.childNodes;
		childNodes.forEach(child=>{
	        if(this.isElementNode(child)){
	        	//是元素节点
	        	this.compileElement(child);
	        }else{
	        	//文本节点
	        	this.compileText(child);
	        }
	        //递归
	        if(child.childNodes && child.childNodes.length){
	        	this.compile(child);
	        }
		})
		//childNodes 打印为一个数组 NodeList(15)[text, h1, text, p, text, ul, text, span, text, span, text, h2, text, input, text]
	}
	isElementNode(el){
		return el.nodeType===1;//如果是节点,nodeType属性===1 原生js
	}
	
	
	//3.1处理元素节点:拿到每个节点的属性,拿到属性,判断属性是否含有指令,根据不同指令使用不同update方法
 compileElement(node){
         const attributes=node.attributes;
         [...attributes].forEach(attr=>{
     	 const {name,value}=attr;
    	if(this.isDirective(name)){//如果是一个指令 v-text v-html on:click ...
     	   const [,dirctive]=name.split('-');//text html on:click
     		
            const [dirName,eventName]=dirctive.split(':');//[text], [on,click]
            //更新数据 数据驱动视图
            
            compileUnit[dirName](node,value,this.vm,eventName);
            // compileUnit:集合了各种指令update方法的对象 
             //哪个节点,指令后跟的值,当前实例,事件名           

            //删除指令属性
            node.removeAttribute('v-'+dirName);
        	}
         })
	}
	isDirective(attrName){
		return attrName.startsWith('v-');
	}
	
	
	//3.2处理文本节点
	compileText(node){
    	    const content=node.textContent;
        	if(/\{\{(.+?)\}\}/.test(content)){//替换{{}}的指令
    		compileUnit['text'](node,content,this.vm);
    	}

	}
	
}

不同指令的对应update方法

const compileUni={
    //根据msg,my.fav从vm.$data中取到对应的val
    getval(expr,vm){
		return expr.split('.').reduce((data,currentVal)=>{ 
             return data[currentVal]
		},vm.$data)
	},
	//处理{{}}的指令,再取得值
	getContentVal(expr,vm){
      return expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
         return this.getval(args[1],vm);
       })
	},
	//更新view层的函数
	updater:{
		textUpdater(node,value){
			node.textContent=value;
		},
		htmlUpdater(node,value){//处理html字符串
			node.innerHTML=value;
		},
		modelUpdater(node,value){
            node.value=value;//node.value 获取文本域的值
		}
	},
	//处理文本节点
	text(node,expr,vm){
	let value;
        if(expr.indexOf('{{')!==-1){
            value= expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
             return this.getval(args[1],vm);
           })
        }else{
         value = this.getval(expr,vm)
        }
        this.updater.textUpdater(node,value)
	},
	html(node,expr,vm){
           const value=this.getval(expr,vm);
           this.updater.htmlUpdater(node,value);
	},
	model(node,expr,vm){
           const value=this.getval(expr,vm);
           this.updater.modelUpdater(node,value);
	},
	on(node,expr,vm,eventName){
         let fn=vm.$options.methods&&vm.$options.methods[expr];
         node.addEventListener(eventName,fn.bind(vm),false);//手动绑上vm
	}
}