1. 程式人生 > 實用技巧 >Vue原始碼解析--實現一個指令解析器 Compile

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