Vue源码解析--实现一个指令解析器 Compile
前言
compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图
大致思路
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将vue实例根节点的el
转换成文档碎片fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实dom节点中
递归遍历保证每个节点及子节点都会解析编译到 , 包括了{{}}表达式声明的文本节点 ,
指令的声明规定是通过特定前缀的节点属性来标记
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <script src="js/MVue.js"></script> <title>Document</title> </head> <body> <div id="example"> <h3> {{person.name}}---{{person.age}}----{{person.fav}} </h3> <ul> <li>1</li> <li>2</li> <li>3</li> </ul> <h2>{{msg}}</h2> <div v-text="person.fav"></div> <div v-text="msg"></div> <div v-html="msg"></div> <input v-model='msg'> <button v-on:click='handle'>测试on</button> <button @click='handle'>测试@</button> </div> <script> //创建实例 new MVue({ el: "#example", data: { msg: 'hello worl', person: { name: 'jack', age: 18, fav: "爱好" } }, methods: { handle() { console.log(this) } }, }) </script> </body> </html>
MVue.js
const compileUtil = { getVal(expr,vm){ return expr.split('.').reduce((data,currentVal) =>{ return data[currentVal] },vm.$data) }, 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); }, bind(node,expr,vm,eventName){ }, updater:{ textUpdater(node,value){ node.textContent = value; }, htmlUpdater(node,value){ node.innerHTML = value; }, modelUpdater(node,value){ node.value = value; } } } class Complie{ constructor(el,vm){ this.el = this.isElementNode(el)? el:document.querySelector(el) this.vm = vm; //1.获取文档碎片对象 ,放入内存中编译,减少回流和重绘 const fragment = this.node2Fragment(this.el); // 2.header编译模板 this.compile(fragment) //3.追加字节点到根元素 this.el.appendChild(fragment) } compile(fragment){ //1.获取每一个子节点 const childNodes = fragment.childNodes; [...childNodes].forEach(child =>{ if (this.isElementNode(child)) { //是元素节点 // console.log(child) //编译元素节点 this.compileElement(child) } else { //是文本节点 // 编译文本节点 this.compileText(child) } if(child.childNodes && child.childNodes.length>0){ this.compile(child) } }) } compileElement(node){ const attributes = node.attributes; [...attributes].forEach(attr=>{ const {name,value} = attr; if (this.isDirective(name)) {//是一个指令 v-model v-text v-on:click const [,direction] = name.split('-');//text on:click const[dirName,eventName] = direction.split(':') ;//分割on:click text html on click //dirName== html || text || no //compileUtil[dirName]需要在compileUtil中分别找到解析html text no 的方法 // 传入 node value 需要知道哪个节点的哪个值 //传入vm 需要在拿到 data中value 对应的值 //eventName 如果有事件 传入事件 // console.log(node,value,this.vm,eventName) compileUtil[dirName](node,value,this.vm,eventName) //策略模式 数据驱动视图 //删除有指令的标签上的属性 node.removeAttribute('v-'+ direction) } else if(this.isEventName(name)){//解 析@符操作 let [,eventName] = name.split('@'); compileUtil['on'](node,value,this.vm,eventName) //策略模式 数据驱动视图 } }) } compileText(node){ const content = node.textContent; if (/\{\{(.+?)\}\}/.test(content)) { compileUtil['text'](node,content,this.vm) //策略模式 数据驱动视图 } } node2Fragment(node){ //创建文档碎片 const f = document.createDocumentFragment(); let firstChild; while(firstChild = node.firstChild){ f.appendChild(firstChild) } return f; } isElementNode(el){ return el.nodeType === 1 } isDirective(attrName){ //判断是否已v-开头 return attrName.startsWith('v-') } isEventName(attrName){ return attrName.startsWith('@') } } class MVue{ constructor(options){ this.$data = options.data; this.$el =options.el; this.$options = options; if (this.$el) { //指令解析器 new Complie(this.$el,this) } } }