Vue原始碼解析--實現一個指令解析器 Compile
阿新 • • 發佈:2020-08-10
前言
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) } } }