大家好,今天我給大家講解一下Vue中模板編譯是如何實現的。
1. 首先我們先創建一個Vue的構造函數,在Vue中,如果有 el 的值我們就 new 一個 Compile 模板的實例,當然這個實例還沒有創建哈!
class Vue{ constructor(options) { this.$el = options.el; this.$data = options.data; //這個根元素存在則編譯模板 if(this.$el){ //模板編譯 new Compiler(this.$el,this); } } }
2. 這個模板編譯呢,主要是有這樣幾步
-
- 判斷 el 是不一個元素,如果是的話,我們直接用就好了,如果不是我們就再去獲取咯!
- 我們要把這個 el 元素中的所有內容全部放到文檔碎片中,這樣的話我們只需要編譯文檔碎片就好了,而不需要去在操作 dom,也就是實現了數據編譯
- 最后我們再把編譯好的文檔碎片放入這個 el 元素中
接下來就讓我們創建一個 Compile 模板的構造函數吧!
class Compile{ constructor(el,vm) { this.vm = vm; //判斷el屬性 是不是一個元素 不是就獲取 this.el = this.isElementNode(el) ? el : document.querySelector(el); console.log(this.el); //把當前的節點元素 獲取到 放到內存中 創建文檔碎片 let fragment = this.node2fragment(this.el); //模板編譯 用數據編譯 this.compile(fragment); //把內容在塞到頁面中 this.el.appendChild(fragment); } }
上面的呢我們是不是用到了幾個方法,當然了,這個方法也還沒有寫呢。。。
這幾個方法呢,都是Compile的原型上的! 我們來寫一下啦!
//獲取所有元素,放到內存中 node2fragment(node){ //創建一個文檔碎片 let fragment = document.createDocumentFragment(); let firstChild; //將node節點的的第一個節點給firstChild 如果node節點的的第一個節點為空則結束 while(firstChild = node.firstChild){ //appendChild具有移動性 fragment.appendChild(firstChild); } return fragment; } // 是不是元素節點 isElementNode(node){ return node.nodeType === 1; }
下面這個方法呢,就是模板的核心方法啦,用它來實現數據編譯
//用來編譯內存中的dom節點 核心方法 compile(node){ let childNodes = node.childNodes; //獲取node的所有子節點 [...childNodes].forEach(child=>{ if(this.isElementNode(child)){ //判斷是不是元素節點 this.compileElement(child); //編譯元素指令 //如果是元素節點的話 需要把自己傳不進去 再去遍歷子元素節點 this.compile(child); }else{ //文本元素 this.compileText(child); //編譯文本指令 } }) }
這個幾個方法就是 compile 這個核心方法所用的方法!
CompileUtil 是全局對象對象,分別儲存對應着不同的方法在下面將會創建
//判斷屬性是不是以 v- 開頭 isDirective(attrName){ return attrName.startsWith('v-'); // return /^v-/.test(attrName) } //編譯元素的 compileElement(node){ let attributes = node.attributes; //類數組, 獲取所有node節點的屬性和屬性值 attributes = [...attributes] //console.log(attributes) attributes.forEach(attr=>{ //是一個屬性對象attr let {name, value:expr} = attr; //:expr是給value起一個別名叫 expr **school.name //判斷是不是vue指令 if(this.isDirective(name)){ let [,directive] = name.split('-'); let [directiveName, eventName] = directive.split(':'); //需要調用不同的指令來處理 *** v-if v-modle v-show v-else CompileUtil[directiveName](node,expr,this.vm,eventName); } }) } //編譯文本的 compileText(node){ //判斷節點中是否包含 {{}} let content = node.textContent; if(/\{\{.+?\}\}/.test(content)){ CompileUtil['text'](node,content,this.vm); } }
因為 vue 中的指令不同,所以我們要調用不同的方法,這里呢,就創建了一個 CompileUtil 的全局對象對象,分別儲存對應着不同的方法
CompileUtil = { //根據表達式獲取對應的數據 getVal(vm,expr){ // reduce() 方法 // 函數的參數 (第一參數)1.相加的初始值,2.循環出來的那一項,3.索引 4.循環的數組 // (第二個參數)初始值 // 返回值:總和的結果 return expr.split('.').reduce((data,current)=>{ //[school,name] return data[current]; },vm.$data); }, model(node,expr,vm){ //node是節點 expr是表達式 vm是實例 let fn = this.updater['modelUpdater']; let value = this.getVal(vm,expr); fn(node,value); }, html(node,expr,vm,eventName){ let fn = this.updater['htmlUpdater'] let value = this.getVal(vm,expr); fn(node,value); }, text(node,expr,vm){ let fn = this.updater['textUpdater'] //console.log(expr) :{{ school.name }} let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"] return this.getVal(vm,args[1].trim()); }) // console.log(content) fn(node,content); }, updater: { //把數據插入到value中 modelUpdater(node,value){ node.value = value; }, htmlUpdater(node,value){ node.innerHTML = value; }, //處理文本節點 textUpdater(node,value){ //textContent 屬性設置或返回指定節點的文本內容,以及它的所有后代。 node.textContent = value; } } }
這樣的話,我們的模板編譯就完成啦!復制代碼去試一下吧!
下面這個是一個模板編譯的整體代碼
/** * 模板編譯 */ class Compiler{ constructor(el,vm) { this.vm = vm; //判斷el屬性 是不是一個元素 不是就獲取 this.el = this.isElementNode(el) ? el : document.querySelector(el); console.log(this.el); //把當前的節點元素 獲取到 放到內存中 創建文檔碎片 let fragment = this.node2fragment(this.el); //把節點中的內容進行替換 //模板編譯 用數據編譯 this.compile(fragment); //把內容在塞到頁面中 this.el.appendChild(fragment); } //判斷屬性是不是以 v- 開頭 isDirective(attrName){ return attrName.startsWith('v-'); // return /^v-/.test(attrName) } //編譯元素的 compileElement(node){ let attributes = node.attributes; //類數組, 獲取所有node節點的屬性和屬性值 attributes = [...attributes] //console.log(attributes) attributes.forEach(attr=>{ //是一個屬性對象attr let {name, value:expr} = attr; //:expr是給value起一個別名叫 expr **school.name //判斷是不是vue指令 if(this.isDirective(name)){ let [,directive] = name.split('-'); let [directiveName, eventName] = directive.split(':'); //需要調用不同的指令來處理 *** v-if v-modle v-show v-else CompileUtil[directiveName](node,expr,this.vm,eventName); } }) } //編譯文本的 compileText(node){ //判斷節點中是否包含 {{}} let content = node.textContent; if(/\{\{.+?\}\}/.test(content)){ CompileUtil['text'](node,content,this.vm); } } //用來編譯內存中的dom節點 核心方法 compile(node){ let childNodes = node.childNodes; //獲取node的所有子節點 [...childNodes].forEach(child=>{ if(this.isElementNode(child)){ //判斷是不是元素節點 this.compileElement(child); //編譯元素指令 //如果是元素節點的話 需要把自己傳不進去 再去遍歷子元素節點 this.compile(child); }else{ //文本元素 this.compileText(child); //編譯文本指令 } }) } //獲取所有元素,放到內存中 node2fragment(node){ //創建一個文檔碎片 let fragment = document.createDocumentFragment(); let firstChild; //將node節點的的第一個節點給firstChild 如果node節點的的第一個節點為空則結束 while(firstChild = node.firstChild){ //appendChild具有移動性 fragment.appendChild(firstChild); } return fragment; } // 是不是元素節點 isElementNode(node){ return node.nodeType === 1; } } CompileUtil = { //根據表達式獲取對應的數據 getVal(vm,expr){ // 7. reduce() 方法 // 函數的參數 (第一參數)1.相加的初始值,2.循環出來的那一項,3.索引 4.循環的數組 // (第二個參數)初始值 // 返回值:總和的結果 return expr.split('.').reduce((data,current)=>{ //[school,name] // console.log(data,current) return data[current]; },vm.$data); }, model(node,expr,vm){ //node是節點 expr是表達式 vm是實例 console.log(node) let fn = this.updater['modelUpdater']; let value = this.getVal(vm,expr); // console.log(value) fn(node,value); }, html(node,expr,vm){ let fn = this.updater['htmlUpdater'] let value = this.getVal(vm,expr); fn(node,value); }, text(node,expr,vm){ let fn = this.updater['textUpdater'] //console.log(expr) :{{ school.name }} let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{ //console.log(args) :["{{ school.name }}", " school.name ", 0, "{{ school.name }}"] return this.getVal(vm,args[1].trim()); }) // console.log(content) fn(node,content); }, updater: { //把數據插入到value中 modelUpdater(node,value){ node.value = value; }, htmlUpdater(node,value){ node.innerHTML = value; }, //處理文本節點 textUpdater(node,value){ //textContent 屬性設置或返回指定節點的文本內容,以及它的所有后代。 node.textContent = value; } } } class Vue{ constructor(options) { this.$el = options.el; this.$data = options.data; //這個根元素存在則編譯模板 if(this.$el){ //模板編譯 new Compiler(this.$el,this); } } }